From cdba4c0d789a201bd2b16e14f173fefc59d1365a Mon Sep 17 00:00:00 2001 From: Yoonyesol <51500821+Yoonyesol@users.noreply.github.com> Date: Mon, 21 Oct 2024 23:29:42 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20#194=20=EC=9C=A0=EC=A0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: #211 유저 프로필 이미지 업로드 기능 구현 * Feat: #211 DB 명세서에 따라 프로필 사진 저장 방식 변경 * Feat: #211 유저 프로필 이미지 저장 형식을 UUID로 변경 * Chore: #211 코드리뷰 반영 수정 * Chore: #211 코드리뷰 반영 수정 * Chore: #211 토큰에서 유저 정보를 추출할 수 없을 때 오류 추가 * Feat: #211 이미지 업로드 시 서버 측에서 UUID를 가져와 zustand 스토어에 저장하도록 수정 * Feat: #194 프로필 이미지 조회 기능 구현 * Chore: #194 파일 경로 수정 * Chore: #194 불필요한 쿼리 함수 삭제 * Chore: #194 msw에서 더미 jwt 코드 삭제 및 접근권한 에러 코드 변경 --- .../user/auth-form/ProfileImageContainer.tsx | 20 ++++++- src/mocks/services/userServiceHandler.ts | 55 ++++++++++++------- src/services/userService.ts | 18 +++++- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/components/user/auth-form/ProfileImageContainer.tsx b/src/components/user/auth-form/ProfileImageContainer.tsx index 6d44482a..fdd5c39c 100644 --- a/src/components/user/auth-form/ProfileImageContainer.tsx +++ b/src/components/user/auth-form/ProfileImageContainer.tsx @@ -4,9 +4,11 @@ import { FaRegTrashCan } from 'react-icons/fa6'; import { convertBytesToString } from '@utils/converter'; import { USER_SETTINGS } from '@constants/settings'; import { JPG, PNG, SVG, WEBP } from '@constants/mimeFileType'; +import useAxios from '@hooks/useAxios'; import useToast from '@hooks/useToast'; import { useUploadProfileImage } from '@hooks/query/useUserQuery'; import useStore from '@stores/useStore'; +import { getProfileImage } from '@services/userService'; type ProfileImageContainerProps = { imageUrl: string | null; @@ -15,8 +17,24 @@ type ProfileImageContainerProps = { export default function ProfileImageContainer({ imageUrl, setImageUrl }: ProfileImageContainerProps) { const { toastWarn } = useToast(); - const { mutate: uploadImageMutate } = useUploadProfileImage(); const { editUserInfo, userInfo } = useStore(); + const { mutate: uploadImageMutate } = useUploadProfileImage(); + const { fetchData } = useAxios(getProfileImage); + const { toastError } = useToast(); + + useEffect(() => { + const handleGetProfileImage = async (uploadName: string) => { + const response = await fetchData(uploadName); + if (response == null) + return toastError('프로필 이미지 조회 도중 문제가 발생했습니다. 잠시 후 다시 시도해주세요.'); + + const blob = new Blob([response.data], { type: response.headers['content-type'] }); + const profileImageUrl = URL.createObjectURL(blob); + setImageUrl(profileImageUrl); + }; + + if (userInfo.profileImageName) handleGetProfileImage(userInfo.profileImageName); + }, [userInfo.profileImageName]); useEffect(() => { return () => { diff --git a/src/mocks/services/userServiceHandler.ts b/src/mocks/services/userServiceHandler.ts index 5e4c7dfd..36719709 100644 --- a/src/mocks/services/userServiceHandler.ts +++ b/src/mocks/services/userServiceHandler.ts @@ -1,12 +1,5 @@ import { http, HttpResponse } from 'msw'; -import { - JWT_TOKEN_DUMMY, - PROFILE_IMAGE_DUMMY, - ROLE_DUMMY, - TEAM_DUMMY, - TEAM_USER_DUMMY, - USER_DUMMY, -} from '@mocks/mockData'; +import { PROFILE_IMAGE_DUMMY, ROLE_DUMMY, TEAM_DUMMY, TEAM_USER_DUMMY, USER_DUMMY } from '@mocks/mockData'; import { NICKNAME_REGEX } from '@constants/regex'; import { convertTokenToUserId } from '@utils/converter'; import { fileNameParser } from '@utils/fileNameParser'; @@ -84,22 +77,12 @@ const userServiceHandler = [ if (!file) return new HttpResponse(null, { status: 400 }); if (!(file instanceof File)) return new HttpResponse('업로드된 문서는 파일이 아닙니다.', { status: 400 }); - let userId; - // ToDo: 추후 삭제 - if (accessToken === JWT_TOKEN_DUMMY) { - const payload = JWT_TOKEN_DUMMY.split('.')[1]; - userId = Number(payload.replace('mocked-payload-', '')); - } else { - // 토큰에서 userId 추출 - userId = convertTokenToUserId(accessToken); - } - + const userId = convertTokenToUserId(accessToken); if (!userId) { return HttpResponse.json({ message: '토큰에 포함된 유저 정보가 존재하지 않습니다.' }, { status: 401 }); } const userIndex = USER_DUMMY.findIndex((user) => user.userId === userId); - if (userIndex === -1) { return HttpResponse.json( { message: '해당 사용자를 찾을 수 없습니다. 입력 정보를 확인해 주세요.' }, @@ -127,6 +110,40 @@ const userServiceHandler = [ return HttpResponse.json({ imageName: uploadName }, { status: 200 }); }), + // 유저 프로필 이미지 조회 API + http.get(`${BASE_URL}/file/profile/:fileName`, async ({ request, params }) => { + const { fileName } = params; + + const accessToken = request.headers.get('Authorization'); + if (!accessToken) return new HttpResponse(null, { status: 401 }); + + const userId = convertTokenToUserId(accessToken); + if (!userId) { + return HttpResponse.json({ message: '토큰에 유저 정보가 존재하지 않습니다.' }, { status: 401 }); + } + + const userIndex = USER_DUMMY.findIndex((user) => user.userId === userId); + if (userIndex === -1) { + return HttpResponse.json( + { message: '해당 사용자를 찾을 수 없습니다. 입력 정보를 확인해 주세요.' }, + { status: 401 }, + ); + } + + const decodedFileName = decodeURIComponent(fileName.toString()); + const fileInfo = PROFILE_IMAGE_DUMMY.find((file) => file.uploadName === decodedFileName); + if (!fileInfo) return new HttpResponse(null, { status: 404 }); + + if (fileInfo.userId !== Number(userId)) + return HttpResponse.json({ message: '해당 파일에 접근 권한이 없습니다.' }, { status: 403 }); + + const buffer = await fileInfo.file.arrayBuffer(); + return HttpResponse.arrayBuffer(buffer, { + headers: { + 'Content-Type': fileInfo.file.type, + }, + }); + }), // 전체 팀 목록 조회 API (가입한 팀, 대기중인 팀) http.get(`${BASE_URL}/user/team`, ({ request }) => { const accessToken = request.headers.get('Authorization'); diff --git a/src/services/userService.ts b/src/services/userService.ts index 00bbf6bc..cf4c2d83 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -30,7 +30,7 @@ export async function updateLinks(links: EditUserLinksForm, axiosConfig: AxiosRe } /** - * 유저 프로필 업로드 API + * 유저 프로필 이미지 업로드 API * * @export * @async @@ -44,6 +44,22 @@ export async function uploadProfileImage(file: File, axiosConfig: AxiosRequestCo return authAxios.postForm(`/user/profile/image`, fileFormData, axiosConfig); } +/** + * 유저 프로필 이미지 조회 API + * + * @export + * @async + * @param {string} fileName - 파일명 + * @param {AxiosRequestConfig} [axiosConfig={}] - axios 요청 옵션 설정 객체 + * @returns {Promise>} + */ +export async function getProfileImage(fileName: string, axiosConfig: AxiosRequestConfig = {}) { + return authAxios.get(`/file/profile/${fileName}`, { + ...axiosConfig, + responseType: 'blob', + }); +} + /** * 유저 목록을 검색하는 API *