From 78111bea56c79826b5f3e81952390eedb94b094c Mon Sep 17 00:00:00 2001 From: Hyejin Yang Date: Sat, 2 Dec 2023 01:25:33 +0900 Subject: [PATCH] =?UTF-8?q?feat(shared):=20=EA=B3=84=EC=A0=95=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=EC=82=AC=EC=A7=84=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(shared): useUploadPhoto hook 이름을 usePhotosUpload 로 변경 * feat(shared): 한 개의 이미지만 관리하는 usePhotoUpload hook 추가 * feat(shelter): 보호소 계정정보 수정 페이지에 프로필 이미지 업로드 기능 추가 * feat(volunteer): 봉사자 계정정보 수정 페이지에 프로필 이미지 업로드 기능 추가 * feat(shared): usePhotosUpload 의 getLocalImageUrls 함수에서 createObjectUrl 을 사용하도록 수정 --- .../src/pages/settings/account/index.tsx | 19 ++--- .../src/pages/volunteers/update/index.tsx | 4 +- .../src/pages/volunteers/write/index.tsx | 4 +- .../src/pages/settings/account/index.tsx | 21 ++--- .../pages/shelters/reviews/update/index.tsx | 4 +- .../pages/shelters/reviews/write/index.tsx | 4 +- packages/shared/hooks/usePhotoUpload.ts | 60 +++++++++++++ .../{useUploadPhoto.ts => usePhotosUpload.ts} | 84 ++++++++----------- 8 files changed, 123 insertions(+), 77 deletions(-) create mode 100644 packages/shared/hooks/usePhotoUpload.ts rename packages/shared/hooks/{useUploadPhoto.ts => usePhotosUpload.ts} (51%) diff --git a/apps/shelter/src/pages/settings/account/index.tsx b/apps/shelter/src/pages/settings/account/index.tsx index e9730a0e..62cf98d6 100644 --- a/apps/shelter/src/pages/settings/account/index.tsx +++ b/apps/shelter/src/pages/settings/account/index.tsx @@ -11,8 +11,9 @@ import { useToast, } from '@chakra-ui/react'; import { useMutation } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { usePhotoUpload } from 'shared/hooks/usePhotoUpload'; import { updateShelterInfo } from '@/apis/shelter'; import { UpdateShelterInfo } from '@/types/apis/shetler'; @@ -21,7 +22,6 @@ import useFetchShelterAccount from './_hooks/useFetchShelterAccount'; export default function SettingsAccountPage() { const toast = useToast(); - const [imgFile, setImgFile] = useState(''); const { data } = useFetchShelterAccount(); const { mutate: updateShelter } = useMutation({ mutationFn: (data: UpdateShelterInfo) => updateShelterInfo(data), @@ -36,22 +36,21 @@ export default function SettingsAccountPage() { }); const { register, handleSubmit, reset, watch } = useForm(); + const { photo, setPhoto, handleUploadPhoto } = usePhotoUpload(data.imageUrl); useEffect(() => { reset(data); - setImgFile(data.imageUrl); + setPhoto(data.imageUrl); }, [data]); const onSubmit = handleSubmit((newData) => { updateShelter(newData); }); - const uploadImgFile = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - const newImgFile = URL.createObjectURL(file); - setImgFile(newImgFile); - } + const uploadImgFile = (event: React.ChangeEvent) => { + const { files } = event.target; + handleUploadPhoto(files); + event.target.value = ''; }; return ( @@ -64,7 +63,7 @@ export default function SettingsAccountPage() { as="label" htmlFor="profileImg" cursor="pointer" - src={imgFile} + src={photo} /> (''); const { data } = useFetchMyVolunteer(); const { mutate: updateAccount } = useMutation({ mutationFn: (data: UpdateUserInfoParams) => updateVolunteerUserInfo(data), @@ -38,6 +38,9 @@ export default function SettingsAccountPage() { }); const { register, handleSubmit, reset, watch } = useForm(); + const { photo, setPhoto, handleUploadPhoto } = usePhotoUpload( + data.volunteerImageUrl, + ); useEffect(() => { reset({ @@ -47,15 +50,13 @@ export default function SettingsAccountPage() { phoneNumber: data.volunteerPhoneNumber, gender: data.volunteerGender, }); - setImgFile(data.volunteerImageUrl); + setPhoto(data.volunteerImageUrl); }, [data, reset]); - const uploadImgFile = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - const newImgFile = URL.createObjectURL(file); - setImgFile(newImgFile); - } + const uploadImgFile = (event: React.ChangeEvent) => { + const { files } = event.target; + handleUploadPhoto(files); + event.target.value = ''; }; const onSubmit = handleSubmit((newData) => { @@ -71,7 +72,7 @@ export default function SettingsAccountPage() { as="label" htmlFor="profileImg" cursor="pointer" - src={imgFile} + src={photo} variant="프로필 이미지" /> { + return { + description, + position: 'top', + status, + duration: 1500, + isClosable: true, + }; +}; + +export const usePhotoUpload = (initialPhoto?: string) => { + const [photo, setPhoto] = useState(initialPhoto); + + const toast = useToast(); + + const handleUploadPhoto = async (files: FileList | null) => { + if (!files) { + return; + } + + if (files.length !== 1) { + toast(toastOption('프로필 이미지는 한 장만 등록 가능합니다.', 'error')); + + return; + } + + const imageFile = files[0]; + const localImageUrl = URL.createObjectURL(imageFile); + setPhoto(localImageUrl); + + const resizedImage = await resizeImageFile(imageFile, 2); + const formData = new FormData(); + formData.append('images', resizedImage); + + const { data } = await uploadImage(formData); + const [imageUrl] = data.imageUrls; + + setPhoto(imageUrl); + }; + + const handleDeletePhoto = () => { + setPhoto(undefined); + }; + + return { + photo, + setPhoto, + handleUploadPhoto, + handleDeletePhoto, + }; +}; diff --git a/packages/shared/hooks/useUploadPhoto.ts b/packages/shared/hooks/usePhotosUpload.ts similarity index 51% rename from packages/shared/hooks/useUploadPhoto.ts rename to packages/shared/hooks/usePhotosUpload.ts index d7ec862b..84e931f0 100644 --- a/packages/shared/hooks/useUploadPhoto.ts +++ b/packages/shared/hooks/usePhotosUpload.ts @@ -24,47 +24,33 @@ const getRandomId = (): string => { return String(Math.random()).slice(2); }; -const getLocalImageUrls = (imageFiles: ImageFile[]): Promise => { - const getLocalImageUrl = imageFiles.map(({ id, image }) => { - const reader = new FileReader(); - - return new Promise((resolve) => { - reader.onloadend = () => { - const imageFileUrl = String(reader.result); - resolve({ id, url: imageFileUrl }); - }; - reader.readAsDataURL(image as File); - }); - }); - - return Promise.all(getLocalImageUrl); -}; - -const getServerImageUrls = (imageFiles: ImageFile[]) => { - const uploadPromises = imageFiles.map(async ({ id, image }) => { - try { - return new Promise((resolve) => { +const getLocalImageUrls = (imageFiles: ImageFile[]): Photo[] => + imageFiles.map(({ id, image }) => ({ id, url: URL.createObjectURL(image) })); + +const getServerImageUrls = async ( + imageFiles: ImageFile[], +): Promise => { + const uploadPromises: Promise[] = imageFiles.map( + async ({ id, image }) => { + try { + const resizedImage = await resizeImageFile(image, 2); const formData = new FormData(); + formData.append('images', resizedImage); - resizeImageFile(image, 2).then((resizedImage: File) => { - formData.append('images', resizedImage); - uploadImage(formData).then(({ data }) => { - const [imageUrl] = data.imageUrls; - resolve({ id, url: imageUrl }); - }); - }); - }); - } catch (error) { - return new Promise((resolve) => { - resolve({ id, url: 'upload-failed' }); - }); - } - }); + const { data } = await uploadImage(formData); + const [imageUrl] = data.imageUrls; + + return { id, url: imageUrl }; + } catch (error) { + return { id, url: 'upload-failed' }; + } + }, + ); return Promise.all(uploadPromises); }; -export const useUploadPhoto = (uploadLimit: number) => { +export const usePhotosUpload = (uploadLimit: number) => { const [photos, setPhotos] = useState([]); const toast = useToast(); @@ -79,7 +65,7 @@ export const useUploadPhoto = (uploadLimit: number) => { setPhotos(newPhotos); }; - const handleUploadPhoto = (files: FileList | null) => { + const handleUploadPhoto = async (files: FileList | null) => { if (!files) { return; } @@ -100,19 +86,19 @@ export const useUploadPhoto = (uploadLimit: number) => { image: file, })); - getLocalImageUrls(imageFiles).then((newPhotos) => { - setPhotos((prevPhotos) => [...newPhotos, ...prevPhotos]); - }); - - getServerImageUrls(imageFiles).then((newPhotos) => { - newPhotos.forEach((newPhoto) => { - setPhotos((prevPhotos) => - prevPhotos.map((photo) => - photo.id === newPhoto.id ? newPhoto : photo, - ), - ); - }); - }); + setPhotos((prevPhotos) => [ + ...getLocalImageUrls(imageFiles), + ...prevPhotos, + ]); + + const newPhotos: Photo[] = await getServerImageUrls(imageFiles); + + setPhotos((prevPhotos) => + prevPhotos.map((photo) => { + const newPhoto = newPhotos.find(({ id }) => photo.id === id); + return newPhoto ? newPhoto : photo; + }), + ); }; const handleDeletePhoto = (photoIndex: number) => {