Skip to content

Commit

Permalink
feat: make image dto to removable
Browse files Browse the repository at this point in the history
  • Loading branch information
ooooorobo committed Nov 9, 2024
1 parent d1fb27b commit d7168c7
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 59 deletions.
21 changes: 18 additions & 3 deletions src/entities/ideal_partner/model/idealPartnerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createStoreContext } from 'src/shared/functions/createStoreContext';
import { MAX_IDEAL_HEIGHT, MIN_IDEAL_HEIGHT } from 'src/processes/ideal_partner/HeightStyleForm/HeightStyleForm';
import { DrinkingDrinkingCategory, ImageDto, ReligionReligionCategory, SmokingSmokingCategory } from 'src/types';
import { useDataUrlListFromFiles } from 'src/shared/functions/useDataUrlListFromFiles';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';

export const REQUIRED_OPTION_MAX_COUNT = 3;

Expand Down Expand Up @@ -46,6 +46,7 @@ type Action = {
setMaxHeight: (value: number) => void;
setStyle: (value: string) => void;
setImages: (getState: (prevFiles: File[]) => File[]) => void;
setImageDtoList: (getState: (prevFiles: ImageDto[]) => ImageDto[]) => void;
setLocation: (value: Location[]) => void;
setHobbies: (hobbies: Hobby[]) => void;
setReligionCategory: (category: ReligionReligionCategory) => void;
Expand Down Expand Up @@ -84,6 +85,7 @@ const createStoreHook = () =>
images: [],
imageDtoList: [],
setImages: (getState) => set({ images: getState(get().images) }),
setImageDtoList: (getState) => set({ imageDtoList: getState(get().imageDtoList) }),
locations: [],
setLocation: (locations) => set({ locations }),
hobbies: [],
Expand Down Expand Up @@ -127,12 +129,25 @@ export const useIdealPartnerImages = () => {
return [
...imageDtoList,
...(urls.filter(Boolean) as string[]).map(
(url) =>
(url, idx) =>
({
imageId: 'null',
imageId: idx.toString(),
url,
}) satisfies ImageDto,
),
];
}, [imageDtoList, urls]);
};

export const useRemoveIdealPartnerImageDto = () => {
const setImageFiles = useIdealPartnerStore((state) => state.setImages);
const setImageDtoList = useIdealPartnerStore((state) => state.setImageDtoList);

return useCallback(
(targetUrl: string, fileIdx?: number) => {
setImageFiles((prev) => prev.filter((_, idx) => idx !== fileIdx));
setImageDtoList((prev) => prev.filter(({ url }) => url !== targetUrl));
},
[setImageDtoList, setImageFiles],
);
};
17 changes: 16 additions & 1 deletion src/entities/profile/model/myProfileStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Mbti } from 'src/shared/vo/mbti';
import { Hobby } from 'src/entities/hobby/types/hobby';
import { createStoreContext } from 'src/shared/functions/createStoreContext';
import { Book, ImageDto, JobJobCategory, Movie, ReligionReligionCategory, SmokingSmokingCategory } from 'src/types';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useDataUrlListFromFiles } from 'src/shared/functions/useDataUrlListFromFiles';

export type MyProfile = {
Expand Down Expand Up @@ -49,6 +49,7 @@ type Action = {
setBirthDate: (date: number) => void;
setHeight: (height: number) => void;
setSelfImages: (getState: (prevFiles: File[]) => File[]) => void;
setImageDtoList: (getState: (prevFiles: ImageDto[]) => ImageDto[]) => void;
setMbti: (mbti: Mbti | null) => void;
setJobCategory: (job: JobJobCategory) => void;
setJobName: (description: string) => void;
Expand Down Expand Up @@ -93,6 +94,7 @@ const createStoreHook = (initialState?: MyProfile) =>
images: [],
imageDtoList: [],
setSelfImages: (getState) => set({ images: getState(get().images) }),
setImageDtoList: (getState) => set({ imageDtoList: getState(get().imageDtoList) }),
mbti: null,
setMbti: (mbti) => set({ mbti }),
job: {
Expand Down Expand Up @@ -171,3 +173,16 @@ export const useMyProfileImages = () => {
];
}, [imageDtoList, urls]);
};

export const useRemoveProfileImageDto = () => {
const setImageFiles = useMyProfileStore((state) => state.setSelfImages);
const setImageDtoList = useMyProfileStore((state) => state.setImageDtoList);

return useCallback(
(targetUrl: string, fileIdx?: number) => {
setImageFiles((prev) => prev.filter((_, idx) => idx !== fileIdx));
setImageDtoList((prev) => prev.filter(({ url }) => url !== targetUrl));
},
[setImageDtoList, setImageFiles],
);
};
51 changes: 51 additions & 0 deletions src/features/upload_image/useUploadProfileImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useCallback, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { ImageDto, uploadImage } from 'src/types';

export const useUploadProfileImage = () => {
const [progress, setProgress] = useState<number>(0);

const { mutateAsync: uploadMutation } = useMutation({
mutationFn: uploadImage,
});

const uploadFileList = useCallback(
async (files: File[], onUpload: () => void) => {
const results: ImageDto[] = [];
// 한번에 너무 많이 요청하지 않도록 하나씩 업로드 요청
for (const file of files) {
try {
const result = await uploadMutation({ image: file });
onUpload();
results.push(result.data);
} catch (e) {
console.error(e);
}
}
return results;
},
[uploadMutation],
);

const upload = useCallback(
async (profileImages: File[], idealPartnerImages: File[]) => {
setProgress(0);

const total = profileImages.length + idealPartnerImages.length + 1;

const [profileImageResults, idealImageResults] = await Promise.all([
uploadFileList(profileImages, () => {
setProgress((prev) => prev + 1 / total);
}),
uploadFileList(idealPartnerImages, () => {
setProgress((prev) => prev + 1 / total);
}),
]);

return { profileImageResults, idealImageResults };
},
[uploadFileList],
);

return { progress, upload };
};
14 changes: 11 additions & 3 deletions src/pages/edit_info/EditInfoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import toast from 'react-hot-toast';
import { useSubmit } from '@remix-run/react';
import { convertProfileToDto } from 'src/entities/profile/model/convertProfileToDto';
import { convertIdealPartnerToDto } from 'src/entities/ideal_partner/model/convertIdealPartnerToDto';
import { useUploadProfileImage } from 'src/features/upload_image/useUploadProfileImage';

type Props = {
infoId: string;
Expand All @@ -23,20 +24,27 @@ export const EditInfoPage = ({ infoId }: Props) => {

const submit = useSubmit();

const { upload } = useUploadProfileImage();
const onCompleteEdit = useCallback((close: () => void) => {
toast.success('변경사항이 저장되었습니다.');
close();
}, []);

const onSubmit = () =>
const onSubmit = useCallback(async () => {
const { profileImageResults, idealImageResults } = await upload(profile.images, idealPartner.images);

const profileImageDtos = [...profile.imageDtoList, ...profileImageResults];
const idealImageDtos = [...idealPartner.imageDtoList, ...idealImageResults];

submit(
{
id: infoId,
userInfo: JSON.stringify(convertProfileToDto(profile, profile.imageDtoList)),
idealPartner: JSON.stringify(convertIdealPartnerToDto(idealPartner, idealPartner.imageDtoList)),
userInfo: JSON.stringify(convertProfileToDto(profile, profileImageDtos)),
idealPartner: JSON.stringify(convertIdealPartnerToDto(idealPartner, idealImageDtos)),
},
{ method: 'post' },
);
}, [idealPartner, infoId, profile, submit, upload]);

return (
<div className={styles.Wrapper}>
Expand Down
42 changes: 4 additions & 38 deletions src/pages/form/complete/UploadLoadingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { convertProfileToDto } from 'src/entities/profile/model/convertProfileTo
import { convertIdealPartnerToDto } from 'src/entities/ideal_partner/model/convertIdealPartnerToDto';
import { useMyProfileStore } from 'src/entities/profile/model/myProfileStore';
import { useIdealPartnerStore } from 'src/entities/ideal_partner/model/idealPartnerStore';
import { useMutation } from '@tanstack/react-query';
import { ImageDto, uploadImage } from 'src/types';
import { Button } from 'src/shared/ui/Button/Button';
import { UploadLoadingPageView } from 'src/pages/form/complete/UploadLoadingPageView';
import { useUploadProfileImage } from 'src/features/upload_image/useUploadProfileImage';

export const UploadLoadingPage = ({
name,
Expand All @@ -26,41 +25,10 @@ export const UploadLoadingPage = ({
const profile = useMyProfileStore((state) => state);
const idealPartner = useIdealPartnerStore((state) => state);

const { mutateAsync: uploadMutation } = useMutation({
mutationFn: uploadImage,
});

const uploadFileList = async (files: File[], onUpload: () => void) => {
const results: ImageDto[] = [];
// 한번에 너무 많이 요청하지 않도록 하나씩 업로드 요청
for (const file of files) {
try {
const result = await uploadMutation({ image: file });
onUpload();
results.push(result.data);
} catch (e) {
console.error(e);
}
}
return results;
};

const [progress, setProgress] = useState<number>(0);
const { progress, upload: uploadFiles } = useUploadProfileImage();

const upload = useCallback(async () => {
const profileImageList = profile.images;
const idealPartnerImageList = idealPartner.images;

const total = profileImageList.length + idealPartnerImageList.length + 1;

const [profileImageResults, idealImageResults] = await Promise.all([
uploadFileList(profileImageList, () => {
setProgress((prev) => prev + 1 / total);
}),
uploadFileList(idealPartnerImageList, () => {
setProgress((prev) => prev + 1 / total);
}),
]);
const { profileImageResults, idealImageResults } = await uploadFiles(profile.images, idealPartner.images);

submit(
{
Expand All @@ -70,10 +38,9 @@ export const UploadLoadingPage = ({
},
{ method: 'post' },
);
}, []);
}, [idealPartner, linkKey, profile, submit, uploadFiles]);

useEffect(() => {
setProgress(0);
upload();
}, []);

Expand Down Expand Up @@ -103,7 +70,6 @@ export const UploadLoadingPage = ({
color={'primary'}
onClick={() => {
setError(false);
setProgress(0);
upload();
}}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import styles from './HeightStyleForm.module.css';
import { useIdealPartnerImages, useIdealPartnerStore } from 'src/entities/ideal_partner/model/idealPartnerStore';
import {
useIdealPartnerImages,
useIdealPartnerStore,
useRemoveIdealPartnerImageDto,
} from 'src/entities/ideal_partner/model/idealPartnerStore';
import { RangeSlider } from 'src/shared/ui/RangeSlider/RangeSlider';
import { Input } from 'src/shared/ui/Input/Input';
import { AvatarList } from 'src/shared/ui/AvatarList/AvatarList';
Expand All @@ -15,6 +19,7 @@ export const HeightStyleForm = () => {
const style = useIdealPartnerStore((state) => state.style);

const imageDtoList = useIdealPartnerImages();
const removeImageDto = useRemoveIdealPartnerImageDto();

const setMin = useIdealPartnerStore((state) => state.setMinHeight);
const setMax = useIdealPartnerStore((state) => state.setMaxHeight);
Expand Down Expand Up @@ -54,7 +59,7 @@ export const HeightStyleForm = () => {
<p className={'label'}>이상형 참고사진</p>
<p className={styles.PictureLabelDescription}>사진은 최대 10장까지 올릴 수 있어요.</p>
</div>
<AvatarList imageDtoList={imageDtoList} setFiles={setFiles} maxFileCount={10} />
<AvatarList imageDtoList={imageDtoList} setFiles={setFiles} maxFileCount={10} onClickRemove={removeImageDto} />
</div>
</section>
);
Expand Down
12 changes: 7 additions & 5 deletions src/processes/my_profile/MyImageForm/MyImageForm.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import styles from './MyImageForm.module.css';
import { InfoBox } from 'src/shared/ui/InfoBox/InfoBox';
import { useMyProfileImages, useMyProfileStore } from 'src/entities/profile/model/myProfileStore';
import {
useMyProfileImages,
useMyProfileStore,
useRemoveProfileImageDto,
} from 'src/entities/profile/model/myProfileStore';
import { AvatarList } from 'src/shared/ui/AvatarList/AvatarList';
import { useMemo } from 'react';

export const MyImageForm = () => {
const files = useMyProfileStore((state) => state.images) ?? [];
const setFiles = useMyProfileStore((state) => state.setSelfImages);
const removeImageDto = useRemoveProfileImageDto();

const dtoList = useMyProfileImages();
const urls = useMemo(() => dtoList.map((dto) => dto.url), [dtoList]);

return (
<section className={styles.Container}>
<AvatarList urls={urls} setFiles={setFiles} maxFileCount={10} />
<AvatarList imageDtoList={dtoList} setFiles={setFiles} maxFileCount={10} onClickRemove={removeImageDto} />
<InfoBox className={styles.InfoBox} radiusSize={'M'}>
<h3>사진 업로드 TIP!</h3>
<div className={styles.InfoWrapper}>
Expand Down
13 changes: 6 additions & 7 deletions src/shared/ui/AvatarList/AvatarList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,34 @@ import { ImageDto } from 'src/types';
type Props = {
imageDtoList: ImageDto[];
setFiles?: (getState: (prevFiles: File[]) => File[]) => void;
onClickRemove?: (url: string, fileIdx?: number) => void;
maxFileCount?: number;
};

export const AvatarList = ({ imageDtoList = [], setFiles, maxFileCount }: Props) => {
export const AvatarList = ({ imageDtoList = [], setFiles, onClickRemove, maxFileCount }: Props) => {
const canAddFile = Boolean(setFiles && (!maxFileCount || imageDtoList.length < maxFileCount));

const onFileChanged = (files: File[]) => {
setFiles?.((prev) => [...prev, ...files].slice(0, maxFileCount));
};

const onClickRemove = (targetIdx: number) => {
setFiles?.((prev) => prev.filter((_, idx) => idx !== targetIdx));
};

return (
<div className={styles.ImageContainer}>
{canAddFile && (
<UploadTrigger onUploadFiles={onFileChanged} accept={'image/*'} multiple>
{(onClickUpload) => <Avatar fallback={<Plus />} shape={'roundedSquare'} size={72} onClick={onClickUpload} />}
</UploadTrigger>
)}
{imageDtoList.map(({ url }, idx) => (
{imageDtoList.map(({ url, imageId }) => (
<AvatarWithModal
key={url}
fallback={''}
shape={'roundedSquare'}
size={72}
src={url}
actionSlot={<Close onClick={() => onClickRemove(idx)} />}
actionSlot={
<Close onClick={() => onClickRemove?.(url, isNaN(Number(imageId)) ? Number(imageId) : undefined)} />
}
/>
))}
</div>
Expand Down

0 comments on commit d7168c7

Please sign in to comment.