Skip to content

Commit

Permalink
대표이미지 수정버그, 아이템 이미지 등록삭제시 생기는 버그 수정 (#715)
Browse files Browse the repository at this point in the history
* refactor:여러이미지업로드 로직 함수분리, 변수명변경

* fix: 대표사진 업로드 버그 수정(맥시멈 사진갯수지정)

* fix: 배열 멱등성 지켜서 버그 방지

* refactor:이미지url 변수명 변경

* refactor: 모바일 모달창 문구보이게 margin추가
  • Loading branch information
Dahyeeee authored Oct 18, 2023
1 parent 1a9ebe0 commit aed8c66
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,7 +65,7 @@ const TripInformation = ({
<Box css={imageWrapperStyling}>
<div />
<img
src={tripData.imageName ? convertImageName(tripData.imageName) : DefaultThumbnail}
src={tripData.imageName ? convertToImageUrl(tripData.imageName) : DefaultThumbnail}
alt="여행 대표 이미지"
/>
</Box>
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/components/common/TripItem/TripItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)}
/>
</div>
)}
Expand Down Expand Up @@ -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) => (
<div css={expandedImageContainer}>
<img src={convertImageName(imageName)} alt="이미지" css={expandedImage} />
<img src={convertToImageUrl(imageName)} alt="이미지" css={expandedImage} />
</div>
))}
</Carousel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Modal
Expand Down Expand Up @@ -119,7 +116,7 @@ const TripInfoEditModal = ({ isOpen, onClose, ...information }: TripInfoEditModa
id="cover-image-upload"
label="대표 이미지 업로드"
imageAltText="여행 대표 업로드 이미지"
imageUrls={uploadedImageName}
imageUrls={imageUrls}
maxUploadCount={1}
onChange={handleImageUpload}
onRemove={handleImageRemoval}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const formStyling = css({

'@media screen and (max-width: 600px)': {
width: `calc(100vw - ${Theme.spacer.spacing7})`,
marginBottom: Theme.spacer.spacing6,

overflowY: 'auto',
'-ms-overflow-style': 'none',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ const TripItemAddModal = ({
createToast('이미지는 최대 5개 업로드할 수 있습니다.');
};

const { isImageUploading, uploadedImageNames, handleImageUpload, handleImageRemoval } =
const { isImageUploading, imageUrls, handleImageUpload, handleImageRemoval } =
useMultipleImageUpload({
initialImageNames: tripItemInformation.imageNames,
onSuccess: handleImageNamesChange,
updateFormImage: handleImageNamesChange,
onError: handleImageUploadError,
});

Expand Down Expand Up @@ -152,7 +152,7 @@ const TripItemAddModal = ({
<ImageUploadInput
id="image-upload"
label="이미지 업로드"
imageUrls={uploadedImageNames}
imageUrls={imageUrls}
imageAltText="여행 일정 업로드 이미지"
supportingText="사진은 최대 5장 올릴 수 있어요."
maxUploadCount={TRIP_ITEM_ADD_MAX_IMAGE_UPLOAD_COUNT}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
nameStyling,
} from '@components/trips/CommunityTripsItem/CommunityTripsItem.style';

import convertImageName from '@utils/convertImageName';
import { convertToImageUrl } from '@utils/convertImage';
import { formatDate } from '@utils/formatter';

import type { CommunityTripsItemData } from '@type/trips';
Expand Down Expand Up @@ -75,7 +75,7 @@ const CommunityTripsItem = ({ index, trip }: CommunityTripsItemProps) => {
css={clickableLikeStyling}
/>
<img
src={coverImage ? convertImageName(coverImage) : DefaultThumbnail}
src={coverImage ? convertToImageUrl(coverImage) : DefaultThumbnail}
css={imageStyling}
alt={`${title} 대표 이미지`}
/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/trips/TripsItem/TripsItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -49,7 +49,7 @@ const TripsItem = ({
onClick={() => navigate(PATH.TRIP(String(id)))}
>
<img
src={coverImage ? convertImageName(coverImage) : DefaultThumbnail}
src={coverImage ? convertToImageUrl(coverImage) : DefaultThumbnail}
css={imageStyling}
alt={`${itemName} 대표 이미지`}
/>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/api/useImageMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
151 changes: 81 additions & 70 deletions frontend/src/hooks/common/useMultipleImageUpload.ts
Original file line number Diff line number Diff line change
@@ -1,130 +1,141 @@
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';

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<HTMLInputElement>) => {
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<File[]> => {
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<HTMLInputElement>) => {
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 };
};
1 change: 0 additions & 1 deletion frontend/src/hooks/trip/useAddTripItemForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export const useAddTripItemForm = ({

return;
}

if (!itemId) {
addTripItemMutation.mutate(
{
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/mocks/data/image.ts
Original file line number Diff line number Diff line change
@@ -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',
];
Loading

0 comments on commit aed8c66

Please sign in to comment.