From aed8c66e05c58704b1a66104f0c6e7b1e07daa66 Mon Sep 17 00:00:00 2001 From: Dahye Yun <102305630+Dahyeeee@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:03:36 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EB=B2=84=EA=B7=B8,=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EC=8B=9C=20=EC=83=9D=EA=B8=B0=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#715)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor:여러이미지업로드 로직 함수분리, 변수명변경 * fix: 대표사진 업로드 버그 수정(맥시멈 사진갯수지정) * fix: 배열 멱등성 지켜서 버그 방지 * refactor:이미지url 변수명 변경 * refactor: 모바일 모달창 문구보이게 margin추가 --- .../TripInformation/TripInformation.tsx | 4 +- .../components/common/TripItem/TripItem.tsx | 9 +- .../TripInfoEditModal/TripInfoEditModal.tsx | 19 +-- .../TripItemAddModal.style.ts | 1 + .../TripItemAddModal/TripItemAddModal.tsx | 6 +- .../CommunityTripsItem/CommunityTripsItem.tsx | 4 +- .../components/trips/TripsItem/TripsItem.tsx | 4 +- frontend/src/hooks/api/useImageMutation.ts | 3 + .../hooks/common/useMultipleImageUpload.ts | 151 ++++++++++-------- frontend/src/hooks/trip/useAddTripItemForm.ts | 1 - frontend/src/mocks/data/image.ts | 7 +- frontend/src/mocks/handlers/tripItem.ts | 2 +- frontend/src/utils/convertImage.ts | 13 ++ frontend/src/utils/convertImageName.ts | 5 - frontend/src/utils/convertImageNames.ts | 5 - 15 files changed, 123 insertions(+), 111 deletions(-) create mode 100644 frontend/src/utils/convertImage.ts delete mode 100644 frontend/src/utils/convertImageName.ts delete mode 100644 frontend/src/utils/convertImageNames.ts diff --git a/frontend/src/components/common/TripInformation/TripInformation.tsx b/frontend/src/components/common/TripInformation/TripInformation.tsx index 32c05eb3f..e16133675 100644 --- a/frontend/src/components/common/TripInformation/TripInformation.tsx +++ b/frontend/src/components/common/TripInformation/TripInformation.tsx @@ -23,7 +23,7 @@ import { useTrip } from '@hooks/trip/useTrip'; import { mediaQueryMobileState } from '@store/mediaQuery'; -import convertImageName from '@utils/convertImageName'; +import { convertToImageUrl } from '@utils/convertImage'; import { formatDate } from '@utils/formatter'; import type { TripData, TripTypeData } from '@type/trip'; @@ -65,7 +65,7 @@ const TripInformation = ({
여행 대표 이미지 diff --git a/frontend/src/components/common/TripItem/TripItem.tsx b/frontend/src/components/common/TripItem/TripItem.tsx index 6a9edffad..519a5451f 100644 --- a/frontend/src/components/common/TripItem/TripItem.tsx +++ b/frontend/src/components/common/TripItem/TripItem.tsx @@ -26,8 +26,7 @@ import useResizeImage from '@hooks/trip/useResizeImage'; import { mediaQueryMobileState } from '@store/mediaQuery'; -import convertImageName from '@utils/convertImageName'; -import convertImageNames from '@utils/convertImageNames'; +import { convertToImageUrl, convertToImageUrls } from '@utils/convertImage'; import { formatNumberToMoney } from '@utils/formatter'; import type { TripItemData } from '@type/tripItem'; @@ -101,7 +100,7 @@ const TripItem = ({ showNavigationOnHover={!isMobile} showArrows={information.imageNames.length > 1} showDots={information.imageNames.length > 1} - images={convertImageNames(information.imageNames)} + images={convertToImageUrls(information.imageNames)} />
)} @@ -147,11 +146,11 @@ const TripItem = ({ showNavigationOnHover={!isMobile} showArrows={information.imageNames.length > 1} showDots={information.imageNames.length > 1} - images={convertImageNames(information.imageNames)} + images={convertToImageUrls(information.imageNames)} > {information.imageNames.map((imageName) => (
- 이미지 + 이미지
))} diff --git a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx index 21840badf..894d8a2e6 100644 --- a/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx +++ b/frontend/src/components/trip/TripInfoEditModal/TripInfoEditModal.tsx @@ -46,22 +46,19 @@ const TripInfoEditModal = ({ isOpen, onClose, ...information }: TripInfoEditModa handleSubmit, } = useTripEditForm(information, onClose); - const handleImageNamesChange = useCallback( + const handleImageNameChange = useCallback( (imageNames: string[]) => { updateCoverImage(imageNames[0]); }, [updateCoverImage] ); - const { - uploadedImageNames: uploadedImageName, - isImageUploading, - handleImageUpload, - handleImageRemoval, - } = useMultipleImageUpload({ - initialImageNames: information.imageName === null ? [] : [information.imageName], - onSuccess: handleImageNamesChange, - }); + const { imageUrls, isImageUploading, handleImageUpload, handleImageRemoval } = + useMultipleImageUpload({ + initialImageNames: information.imageName === null ? [] : [information.imageName], + updateFormImage: handleImageNameChange, + maxUploadCount: 1, + }); return ( { css={clickableLikeStyling} /> {`${title} diff --git a/frontend/src/components/trips/TripsItem/TripsItem.tsx b/frontend/src/components/trips/TripsItem/TripsItem.tsx index 52f7bc5df..f3cd87ff3 100644 --- a/frontend/src/components/trips/TripsItem/TripsItem.tsx +++ b/frontend/src/components/trips/TripsItem/TripsItem.tsx @@ -10,7 +10,7 @@ import { nameStyling, } from '@components/trips/TripsItem/TripsItem.style'; -import convertImageName from '@utils/convertImageName'; +import { convertToImageUrl } from '@utils/convertImage'; import type { CityData } from '@type/city'; @@ -49,7 +49,7 @@ const TripsItem = ({ onClick={() => navigate(PATH.TRIP(String(id)))} > {`${itemName} diff --git a/frontend/src/hooks/api/useImageMutation.ts b/frontend/src/hooks/api/useImageMutation.ts index 3296075ba..ae76d0bc5 100644 --- a/frontend/src/hooks/api/useImageMutation.ts +++ b/frontend/src/hooks/api/useImageMutation.ts @@ -14,6 +14,9 @@ export const useImageMutation = () => { const imageMutation = useMutation({ mutationFn: postImage, + onSuccess: () => { + createToast('이미지 업로드에 성공했습니다', 'success'); + }, onError: (error: ErrorResponseData) => { if (error.code && error.code > ERROR_CODE.TOKEN_ERROR_RANGE) { handleTokenError(); diff --git a/frontend/src/hooks/common/useMultipleImageUpload.ts b/frontend/src/hooks/common/useMultipleImageUpload.ts index a17e755d4..df6151e66 100644 --- a/frontend/src/hooks/common/useMultipleImageUpload.ts +++ b/frontend/src/hooks/common/useMultipleImageUpload.ts @@ -1,12 +1,11 @@ import type { ChangeEvent } from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import imageCompression from 'browser-image-compression'; import { useImageMutation } from '@hooks/api/useImageMutation'; -import { useToast } from '@hooks/common/useToast'; -import convertImageNames from '@utils/convertImageNames'; +import { convertToImageNames, convertToImageUrls } from '@utils/convertImage'; import { IMAGE_COMPRESSION_OPTIONS } from '@constants/image'; import { TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT } from '@constants/ui'; @@ -14,117 +13,129 @@ import { TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT } from '@constants/ui'; interface UseMultipleImageUploadParams { initialImageNames: string[]; maxUploadCount?: number; - handleInitialImage?: (images: string[]) => void; - onSuccess?: CallableFunction; + updateFormImage?: CallableFunction; onError?: CallableFunction; } export const useMultipleImageUpload = ({ initialImageNames, maxUploadCount = TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT, - onSuccess, + updateFormImage, onError, }: UseMultipleImageUploadParams) => { const imageMutation = useImageMutation(); const isImageUploading = imageMutation.isLoading; - const convertedImageNames = convertImageNames(initialImageNames); + const initialImageUrls = convertToImageUrls([...initialImageNames]); - const { createToast } = useToast(); - const [uploadedImageNames, setUploadedImageNames] = useState(convertedImageNames); + const [imageUrls, setImageUrls] = useState(initialImageUrls); + const [imageNames, setImageNames] = useState(initialImageNames); - const handleImageUpload = useCallback( - async (event: ChangeEvent) => { - const originalImageFiles = event.target.files; - - if (!originalImageFiles) return; + useEffect(() => { + updateFormImage?.(imageNames); + }, [imageNames, updateFormImage]); - if (originalImageFiles.length + uploadedImageNames.length > maxUploadCount) { - onError?.(); + const compressImages = useCallback(async (originalImageFiles: FileList): Promise => { + const imageFiles: File[] = []; - return; - } + try { + await Promise.all( + [...originalImageFiles].map(async (file) => { + const compressedImageFile = await imageCompression(file, IMAGE_COMPRESSION_OPTIONS); - const prevImageNames = uploadedImageNames; + const fileName = file.name; + const fileType = compressedImageFile.type; + const convertedFile = new File([compressedImageFile], fileName, { type: fileType }); - setUploadedImageNames((prevImageNames) => { - const newImageNames = [...originalImageFiles].map((file) => URL.createObjectURL(file)); - - return [...prevImageNames, ...newImageNames]; - }); - - const imageFiles: File[] = []; - - try { - await Promise.all( - [...originalImageFiles].map(async (file) => { - const compressedImageFile = await imageCompression(file, IMAGE_COMPRESSION_OPTIONS); - - const fileName = file.name; - const fileType = compressedImageFile.type; - const convertedFile = new File([compressedImageFile], fileName, { type: fileType }); + imageFiles.push(convertedFile); + }) + ); + } catch (e) { + imageFiles.push(...originalImageFiles); + } - imageFiles.push(convertedFile); - }) - ); - } catch (e) { - imageFiles.push(...originalImageFiles); - } + return imageFiles; + }, []); - const imageUploadFormData = new FormData(); + const convertToImageFormData = useCallback( + async (imageFiles: FileList) => { + const compressedImages = await compressImages(imageFiles); + const imageFormData = new FormData(); - [...imageFiles].forEach((file) => { - imageUploadFormData.append('images', file); + compressedImages.forEach((file) => { + imageFormData.append('images', file); }); + return imageFormData; + }, + [compressImages] + ); + + const postImageNames = useCallback( + async (images: FormData) => { imageMutation.mutate( - { images: imageUploadFormData }, + { images }, { onSuccess: ({ imageNames }) => { if (maxUploadCount === 1) { - onSuccess?.([...imageNames]); - createToast('이미지 업로드에 성공했습니다', 'success'); + setImageNames([...imageNames]); return; } - onSuccess?.([...convertedImageNames, ...imageNames]); - createToast('이미지 업로드에 성공했습니다', 'success'); + setImageNames((prev) => [...prev, ...imageNames]); }, onError: () => { - setUploadedImageNames(prevImageNames); + setImageUrls(initialImageUrls); }, } ); + }, + [imageMutation, maxUploadCount, initialImageUrls] + ); + + const handleImageUpload = useCallback( + async (event: ChangeEvent) => { + const originalImageFiles = event.target.files; + + if (!originalImageFiles) return; + + if (originalImageFiles.length + imageUrls.length > maxUploadCount) { + onError?.(); + + return; + } + + // 화면에 보여지는 이미지 url로 변경 + 업데이트 + setImageUrls((prevImageUrls) => { + const newImageUrls = [...originalImageFiles].map((file) => URL.createObjectURL(file)); + + return [...prevImageUrls, ...newImageUrls]; + }); + + const imageFormData = await convertToImageFormData(originalImageFiles); + postImageNames(imageFormData); // eslint-disable-next-line no-param-reassign event.target.value = ''; }, - [ - createToast, - imageMutation, - convertedImageNames, - maxUploadCount, - onError, - onSuccess, - uploadedImageNames, - ] + [imageUrls, maxUploadCount, convertToImageFormData, postImageNames, onError] ); const handleImageRemoval = useCallback( - (selectedImageName: string) => () => { - setUploadedImageNames((prevImageNames) => { - const updatedImageNames = prevImageNames.filter( - (imageName) => imageName !== selectedImageName - ); - - onSuccess?.(updatedImageNames); - - return updatedImageNames; + (selectedImageUrl: string) => () => { + setImageUrls((prevImageUrls) => { + const updatedImageUrls = prevImageUrls.filter((imageUrl) => imageUrl !== selectedImageUrl); + // form에 들어가는 imageName 변경 + const imageNames = convertToImageNames(updatedImageUrls); + updateFormImage?.(imageNames); + + // 화면에 보여지는 imageUrl 변경 + return updatedImageUrls; }); }, - [onSuccess] + [updateFormImage] ); - return { isImageUploading, uploadedImageNames, handleImageUpload, handleImageRemoval }; + return { isImageUploading, imageUrls, handleImageUpload, handleImageRemoval }; }; diff --git a/frontend/src/hooks/trip/useAddTripItemForm.ts b/frontend/src/hooks/trip/useAddTripItemForm.ts index 091ef6cee..90c1c4cd6 100644 --- a/frontend/src/hooks/trip/useAddTripItemForm.ts +++ b/frontend/src/hooks/trip/useAddTripItemForm.ts @@ -107,7 +107,6 @@ export const useAddTripItemForm = ({ return; } - if (!itemId) { addTripItemMutation.mutate( { diff --git a/frontend/src/mocks/data/image.ts b/frontend/src/mocks/data/image.ts index 4355c8ecc..1d3624a76 100644 --- a/frontend/src/mocks/data/image.ts +++ b/frontend/src/mocks/data/image.ts @@ -1,6 +1,5 @@ export const images = [ - 'https://upload.wikimedia.org/wikipedia/commons/f/f9/Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', - 'https://e3.365dm.com/17/10/2048x1152/skynews-piccadilly-piccadilly-circus_4131587.jpg', - 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/15/9e/a5/6f/regent-str.jpg?w=1200&h=-1&s=1', - 'https://flashbak.com/wp-content/uploads/2017/01/Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', + 'Open_Happiness_Piccadilly_Circus_Blue-Pink_Hour_120917-1126-jikatu.jpg', + 'regent-str.jpg', + 'Piccadilly-by-Night-London-1960-by-Elmar-Ludwig.jpg', ]; diff --git a/frontend/src/mocks/handlers/tripItem.ts b/frontend/src/mocks/handlers/tripItem.ts index 4a8a5369f..872cec452 100644 --- a/frontend/src/mocks/handlers/tripItem.ts +++ b/frontend/src/mocks/handlers/tripItem.ts @@ -39,7 +39,7 @@ export const tripItemHandlers = [ }, } : null, - imageNames: [], + imageNames: response.imageNames, }; trip.dayLogs[0].items.push(newTripItem); diff --git a/frontend/src/utils/convertImage.ts b/frontend/src/utils/convertImage.ts new file mode 100644 index 000000000..c8b75b599 --- /dev/null +++ b/frontend/src/utils/convertImage.ts @@ -0,0 +1,13 @@ +export const convertToImageUrl = (imageName: string | null) => { + return `${process.env.IMAGE_BASEURL}${imageName}`; +}; + +export const convertToImageUrls = (imageNames: string[]) => { + return [...imageNames]?.map((imageName) => `${process.env.IMAGE_BASEURL}${imageName}`); +}; + +export const convertToImageNames = (imageUrls: string[]) => { + return [...imageUrls]?.map((imageUrl) => + imageUrl.replace('blob:', '').replace(`${process.env.IMAGE_BASEURL}`, '') + ); +}; diff --git a/frontend/src/utils/convertImageName.ts b/frontend/src/utils/convertImageName.ts deleted file mode 100644 index d299825c9..000000000 --- a/frontend/src/utils/convertImageName.ts +++ /dev/null @@ -1,5 +0,0 @@ -const convertImageName = (imageName: string | null) => { - return `${process.env.IMAGE_BASEURL}${imageName}`; -}; - -export default convertImageName; diff --git a/frontend/src/utils/convertImageNames.ts b/frontend/src/utils/convertImageNames.ts deleted file mode 100644 index 10ff859ba..000000000 --- a/frontend/src/utils/convertImageNames.ts +++ /dev/null @@ -1,5 +0,0 @@ -const convertImageNames = (imageName: string[]) => { - return imageName?.map((imageName) => `${process.env.IMAGE_BASEURL}${imageName}`); -}; - -export default convertImageNames;