Skip to content

Commit

Permalink
feat(shared): 계정정보 수정 페이지 프로필사진 업로드 기능 추가 (#279)
Browse files Browse the repository at this point in the history
* refactor(shared): useUploadPhoto hook 이름을 usePhotosUpload 로 변경

* feat(shared): 한 개의 이미지만 관리하는 usePhotoUpload hook 추가

* feat(shelter): 보호소 계정정보 수정 페이지에 프로필 이미지 업로드 기능 추가

* feat(volunteer): 봉사자 계정정보 수정 페이지에 프로필 이미지 업로드 기능 추가

* feat(shared): usePhotosUpload 의 getLocalImageUrls 함수에서 createObjectUrl 을 사용하도록 수정
  • Loading branch information
kutta97 authored Dec 1, 2023
1 parent 8301bd3 commit 78111be
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 77 deletions.
19 changes: 9 additions & 10 deletions apps/shelter/src/pages/settings/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,7 +22,6 @@ import useFetchShelterAccount from './_hooks/useFetchShelterAccount';

export default function SettingsAccountPage() {
const toast = useToast();
const [imgFile, setImgFile] = useState<string>('');
const { data } = useFetchShelterAccount();
const { mutate: updateShelter } = useMutation({
mutationFn: (data: UpdateShelterInfo) => updateShelterInfo(data),
Expand All @@ -36,22 +36,21 @@ export default function SettingsAccountPage() {
});

const { register, handleSubmit, reset, watch } = useForm<UpdateShelterInfo>();
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<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const newImgFile = URL.createObjectURL(file);
setImgFile(newImgFile);
}
const uploadImgFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;
handleUploadPhoto(files);
event.target.value = '';
};

return (
Expand All @@ -64,7 +63,7 @@ export default function SettingsAccountPage() {
as="label"
htmlFor="profileImg"
cursor="pointer"
src={imgFile}
src={photo}
/>
<Input
id="profileImg"
Expand Down
4 changes: 2 additions & 2 deletions apps/shelter/src/pages/volunteers/update/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { useCallback, useEffect } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
import { useUploadPhoto } from 'shared/hooks/useUploadPhoto';
import { usePhotosUpload } from 'shared/hooks/usePhotosUpload';
import * as z from 'zod';

import { updateShelterRecruitment } from '@/apis/recruitment';
Expand Down Expand Up @@ -77,7 +77,7 @@ export default function VolunteersUpdatePage() {
resolver: zodResolver(recruitmentSchema),
});
const { photos, setImageUrls, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
usePhotosUpload(UPLOAD_LIMIT);

const contentLength = watch('content')?.length ?? 0;

Expand Down
4 changes: 2 additions & 2 deletions apps/shelter/src/pages/volunteers/write/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useMutation } from '@tanstack/react-query';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
import { useUploadPhoto } from 'shared/hooks/useUploadPhoto';
import { usePhotosUpload } from 'shared/hooks/usePhotosUpload';
import * as z from 'zod';

import { createShelterRecruitment } from '@/apis/recruitment';
Expand Down Expand Up @@ -60,7 +60,7 @@ export default function VolunteersWritePage() {
});

const { photos, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
usePhotosUpload(UPLOAD_LIMIT);

const contentLength = watch('content')?.length ?? 0;

Expand Down
21 changes: 11 additions & 10 deletions apps/volunteer/src/pages/settings/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,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 {
UpdateUserInfoParams,
Expand All @@ -23,7 +24,6 @@ import useFetchMyVolunteer from '@/pages/my/_hooks/useFetchMyVolunteer';

export default function SettingsAccountPage() {
const toast = useToast();
const [imgFile, setImgFile] = useState<string>('');
const { data } = useFetchMyVolunteer();
const { mutate: updateAccount } = useMutation({
mutationFn: (data: UpdateUserInfoParams) => updateVolunteerUserInfo(data),
Expand All @@ -38,6 +38,9 @@ export default function SettingsAccountPage() {
});
const { register, handleSubmit, reset, watch } =
useForm<UpdateUserInfoParams>();
const { photo, setPhoto, handleUploadPhoto } = usePhotoUpload(
data.volunteerImageUrl,
);

useEffect(() => {
reset({
Expand All @@ -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<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const newImgFile = URL.createObjectURL(file);
setImgFile(newImgFile);
}
const uploadImgFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;
handleUploadPhoto(files);
event.target.value = '';
};

const onSubmit = handleSubmit((newData) => {
Expand All @@ -71,7 +72,7 @@ export default function SettingsAccountPage() {
as="label"
htmlFor="profileImg"
cursor="pointer"
src={imgFile}
src={photo}
variant="프로필 이미지"
/>
<Input
Expand Down
4 changes: 2 additions & 2 deletions apps/volunteer/src/pages/shelters/reviews/update/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
import ProfileInfo from 'shared/components/ProfileInfo';
import { useUploadPhoto } from 'shared/hooks/useUploadPhoto';
import { usePhotosUpload } from 'shared/hooks/usePhotosUpload';

import { getVolunteerReviewDetail, updateVolunteerReview } from '@/apis/review';
import { getSimpleShelterProfile } from '@/apis/shelter';
Expand Down Expand Up @@ -94,7 +94,7 @@ export default function SheltersReviewsUpdatePage() {
});

const { photos, setImageUrls, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
usePhotosUpload(UPLOAD_LIMIT);

const contentLength = watch('content')?.length ?? 0;

Expand Down
4 changes: 2 additions & 2 deletions apps/volunteer/src/pages/shelters/reviews/write/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import EditPhotoList from 'shared/components/EditPhotoList';
import ProfileInfo from 'shared/components/ProfileInfo';
import { useUploadPhoto } from 'shared/hooks/useUploadPhoto';
import { usePhotosUpload } from 'shared/hooks/usePhotosUpload';

import { createVolunteerReview } from '@/apis/review';
import { getSimpleShelterProfile } from '@/apis/shelter';
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function SheltersReviewsWritePage() {
});

const { photos, handleUploadPhoto, handleDeletePhoto } =
useUploadPhoto(UPLOAD_LIMIT);
usePhotosUpload(UPLOAD_LIMIT);

const contentLength = watch('content')?.length ?? 0;

Expand Down
60 changes: 60 additions & 0 deletions packages/shared/hooks/usePhotoUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useToast, UseToastOptions } from '@chakra-ui/react';
import { useState } from 'react';

import { uploadImage } from '../apis/common/Image';
import { resizeImageFile } from '../utils/image';

const toastOption = (
description: string,
status: UseToastOptions['status'],
): UseToastOptions => {
return {
description,
position: 'top',
status,
duration: 1500,
isClosable: true,
};
};

export const usePhotoUpload = (initialPhoto?: string) => {
const [photo, setPhoto] = useState<string | undefined>(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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,47 +24,33 @@ const getRandomId = (): string => {
return String(Math.random()).slice(2);
};

const getLocalImageUrls = (imageFiles: ImageFile[]): Promise<Photo[]> => {
const getLocalImageUrl = imageFiles.map(({ id, image }) => {
const reader = new FileReader();

return new Promise<Photo>((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<Photo>((resolve) => {
const getLocalImageUrls = (imageFiles: ImageFile[]): Photo[] =>
imageFiles.map(({ id, image }) => ({ id, url: URL.createObjectURL(image) }));

const getServerImageUrls = async (
imageFiles: ImageFile[],
): Promise<Photo[]> => {
const uploadPromises: Promise<Photo>[] = 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<Photo>((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<Photo[]>([]);

const toast = useToast();
Expand All @@ -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;
}
Expand All @@ -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) => {
Expand Down

0 comments on commit 78111be

Please sign in to comment.