From fd6a7a887143ae52687150237fcdc129cd64ec5f Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:04:47 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/common/useImageUploader.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/frontend/src/hooks/common/useImageUploader.ts b/frontend/src/hooks/common/useImageUploader.ts index 71318f1c1..c5783ad59 100644 --- a/frontend/src/hooks/common/useImageUploader.ts +++ b/frontend/src/hooks/common/useImageUploader.ts @@ -1,17 +1,7 @@ import { useState } from 'react'; -import { uuid } from '@/utils/uuid'; - const isImageFile = (file: File) => file.type !== 'image/png' && file.type !== 'image/jpeg'; -const editImageFileName = (imageFile: File) => { - const fileName = imageFile.name; - const fileExtension = fileName.split('.').pop(); - const newFileName = `${uuid()}.${fileExtension}`; - - return new File([imageFile], newFileName, { type: imageFile.type }); -}; - const useImageUploader = () => { const [imageFile, setImageFile] = useState(null); const [previewImage, setPreviewImage] = useState(''); @@ -22,10 +12,8 @@ const useImageUploader = () => { return; } - const editedImageFile = editImageFileName(imageFile); - - setPreviewImage(URL.createObjectURL(editedImageFile)); - setImageFile(editedImageFile); + setPreviewImage(URL.createObjectURL(imageFile)); + setImageFile(imageFile); }; const deleteImage = () => { From 3afd7bd13dafc6612b4bfd7dc93d9edc658c1537 Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:05:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20useFormData=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/common/index.ts | 1 + frontend/src/hooks/common/useFormData.ts | 29 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 frontend/src/hooks/common/useFormData.ts diff --git a/frontend/src/hooks/common/index.ts b/frontend/src/hooks/common/index.ts index 636922480..0f4756f2a 100644 --- a/frontend/src/hooks/common/index.ts +++ b/frontend/src/hooks/common/index.ts @@ -5,5 +5,6 @@ export { default as useRoutePage } from './useRoutePage'; export { default as useScroll } from './useScroll'; export { default as useSortOption } from './useSortOption'; export { default as useImageUploader } from './useImageUploader'; +export { default as useFormData } from './useFormData'; export { default as useTimeout } from './useTimeout'; export { default as useRouteChangeTracker } from './useRouteChangeTracker'; diff --git a/frontend/src/hooks/common/useFormData.ts b/frontend/src/hooks/common/useFormData.ts new file mode 100644 index 000000000..37cb7011f --- /dev/null +++ b/frontend/src/hooks/common/useFormData.ts @@ -0,0 +1,29 @@ +interface useFormDataProps { + imageKey: string; + imageFile: File | File[] | null; + formContentKey: string; + formContent: T; +} + +const useFormData = ({ imageKey, imageFile, formContentKey, formContent }: useFormDataProps) => { + const formData = new FormData(); + const formContentString = JSON.stringify(formContent); + const formContentBlob = new Blob([formContentString], { type: 'application/json' }); + + formData.append(formContentKey, formContentBlob); + + if (!imageFile) return formData; + + if (Array.isArray(imageFile)) { + imageFile.forEach((file) => { + formData.append(imageKey, file, file.name); + }); + return formData; + } + + formData.append(imageKey, imageFile, imageFile.name); + + return formData; +}; + +export default useFormData; From 78635f3406f2eff16175d7dc7f5a9e45743f02c5 Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:05:35 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EA=BF=80=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=ED=8F=BC=20=EC=9A=94=EC=B2=AD=20=ED=8F=BC=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecipeRegisterForm/RecipeRegisterForm.tsx | 44 +++++++++---------- frontend/src/contexts/RecipeFormContext.tsx | 12 +++-- .../recipe/useRecipeRegisterFormMutation.ts | 5 +-- frontend/src/types/recipe.ts | 3 +- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx b/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx index 012877dce..96f13fad4 100644 --- a/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx +++ b/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx @@ -7,10 +7,10 @@ import RecipeNameInput from '../RecipeNameInput/RecipeNameInput'; import RecipeUsedProducts from '../RecipeUsedProducts/RecipeUsedProducts'; import { ImageUploader, SvgIcon } from '@/components/Common'; -import { useImageUploader } from '@/hooks/common'; +import { useImageUploader, useFormData } from '@/hooks/common'; import { useRecipeFormValueContext, useRecipeFormActionContext } from '@/hooks/context'; import { useRecipeRegisterFormMutation } from '@/hooks/queries/recipe'; -import { useS3Upload } from '@/hooks/s3'; +import type { RecipeRequest } from '@/types/recipe'; interface RecipeRegisterFormProps { closeRecipeDialog: () => void; @@ -20,12 +20,18 @@ const RecipeRegisterForm = ({ closeRecipeDialog }: RecipeRegisterFormProps) => { const theme = useTheme(); const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); - const { uploadToS3, fileUrl } = useS3Upload(imageFile); const recipeFormValue = useRecipeFormValueContext(); const { resetRecipeFormValue } = useRecipeFormActionContext(); - const { mutateAsync } = useRecipeRegisterFormMutation(); + const formData = useFormData({ + imageKey: 'images', + imageFile: imageFile === null ? imageFile : [imageFile], + formContentKey: 'recipeRequest', + formContent: recipeFormValue, + }); + + const { mutate } = useRecipeRegisterFormMutation(); const isValid = recipeFormValue.title.length > 0 && recipeFormValue.content.length > 0 && recipeFormValue.productIds.length > 0; @@ -39,26 +45,20 @@ const RecipeRegisterForm = ({ closeRecipeDialog }: RecipeRegisterFormProps) => { const handleRecipeFormSubmit: FormEventHandler = async (event) => { event.preventDefault(); - try { - await uploadToS3(); - await mutateAsync( - { ...recipeFormValue, images: fileUrl !== null ? [fileUrl] : null }, - { - onSuccess: () => { - resetAndCloseForm(); - }, + mutate(formData, { + onSuccess: () => { + resetAndCloseForm(); + }, + onError: (error) => { + resetAndCloseForm(); + if (error instanceof Error) { + alert(error.message); + return; } - ); - } catch (error) { - resetAndCloseForm(); - - if (error instanceof Error) { - alert(error.message); - return; - } - alert('꿀조합 등록을 다시 시도해주세요.'); - } + alert('꿀조합 등록을 다시 시도해주세요'); + }, + }); }; return ( diff --git a/frontend/src/contexts/RecipeFormContext.tsx b/frontend/src/contexts/RecipeFormContext.tsx index a926f4701..705eb139e 100644 --- a/frontend/src/contexts/RecipeFormContext.tsx +++ b/frontend/src/contexts/RecipeFormContext.tsx @@ -1,12 +1,10 @@ import type { PropsWithChildren } from 'react'; import { createContext, useState } from 'react'; -import type { RecipeRequest } from '@/types/recipe'; - -type RecipeValue = Omit; +import type { RecipeRequest, RecipeRequestKey } from '@/types/recipe'; interface RecipeFormActionParams { - target: keyof RecipeValue; + target: RecipeRequestKey; value: string | number; action?: 'add' | 'remove'; } @@ -16,17 +14,17 @@ interface RecipeFormAction { resetRecipeFormValue: () => void; } -const initialRecipeFormValue: RecipeValue = { +const initialRecipeFormValue: RecipeRequest = { title: '', productIds: [], content: '', }; -export const RecipeFormValueContext = createContext(null); +export const RecipeFormValueContext = createContext(null); export const RecipeFormActionContext = createContext(null); const RecipeFormProvider = ({ children }: PropsWithChildren) => { - const [recipeFormValue, setRecipeFormValue] = useState({ + const [recipeFormValue, setRecipeFormValue] = useState({ title: '', productIds: [], content: '', diff --git a/frontend/src/hooks/queries/recipe/useRecipeRegisterFormMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeRegisterFormMutation.ts index 8b9e233ff..32f6551dd 100644 --- a/frontend/src/hooks/queries/recipe/useRecipeRegisterFormMutation.ts +++ b/frontend/src/hooks/queries/recipe/useRecipeRegisterFormMutation.ts @@ -2,15 +2,12 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { recipeApi } from '@/apis'; -import type { RecipeRequest } from '@/types/recipe'; - -const headers = { 'Content-Type': 'application/json' }; const useRecipeRegisterFormMutation = () => { const navigate = useNavigate(); return useMutation({ - mutationFn: (data: RecipeRequest) => recipeApi.post({ credentials: true }, headers, data), + mutationFn: (data: FormData) => recipeApi.postData({ credentials: true }, data), onSuccess: (response) => { const location = response.headers.get('Location'); if (!location) return; diff --git a/frontend/src/types/recipe.ts b/frontend/src/types/recipe.ts index bce3adfb5..3fec2ba91 100644 --- a/frontend/src/types/recipe.ts +++ b/frontend/src/types/recipe.ts @@ -5,9 +5,10 @@ export interface RecipeRequest { title: string; productIds: number[]; content: string; - images: string[] | null; } +export type RecipeRequestKey = keyof RecipeRequest; + export interface RecipeDetail extends Recipe { images: string[]; content: string; From 6bd466ae547b5d148e8569dd17991d7cb39db93b Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:10:35 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20=ED=8F=BC?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=ED=8F=BC=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewRegisterForm/ReviewRegisterForm.tsx | 47 +++++++++---------- frontend/src/contexts/ReviewFormContext.tsx | 10 ++-- .../review/useReviewRegisterFormMutation.ts | 6 +-- frontend/src/types/review.ts | 6 +-- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx index a0c537423..5956d2cae 100644 --- a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx +++ b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx @@ -10,11 +10,11 @@ import StarRate from '../StarRate/StarRate'; import { ImageUploader, SvgIcon } from '@/components/Common'; import { ProductOverviewItem } from '@/components/Product'; import { MIN_DISPLAYED_TAGS_LENGTH } from '@/constants'; -import { useImageUploader, useScroll } from '@/hooks/common'; +import { useFormData, useImageUploader, useScroll } from '@/hooks/common'; import { useReviewFormActionContext, useReviewFormValueContext } from '@/hooks/context'; import { useProductDetailQuery } from '@/hooks/queries/product'; import { useReviewRegisterFormMutation } from '@/hooks/queries/review'; -import { useS3Upload } from '@/hooks/s3'; +import type { ReviewRequest } from '@/types/review'; const MIN_RATING_SCORE = 0; const MIN_SELECTED_TAGS_COUNT = 1; @@ -28,15 +28,13 @@ interface ReviewRegisterFormProps { const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewRegisterFormProps) => { const { scrollToPosition } = useScroll(); - const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); - const { uploadToS3, fileUrl } = useS3Upload(imageFile); const reviewFormValue = useReviewFormValueContext(); const { resetReviewFormValue } = useReviewFormActionContext(); const { data: productDetail } = useProductDetailQuery(productId); - const { mutateAsync } = useReviewRegisterFormMutation(productId); + const { mutate } = useReviewRegisterFormMutation(productId); const isValid = reviewFormValue.rating > MIN_RATING_SCORE && @@ -44,6 +42,13 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewR reviewFormValue.tagIds.length <= MIN_DISPLAYED_TAGS_LENGTH && reviewFormValue.content.length > MIN_CONTENT_LENGTH; + const formData = useFormData({ + imageKey: 'image', + imageFile: imageFile, + formContentKey: 'reviewRequest', + formContent: reviewFormValue, + }); + const resetAndCloseForm = () => { deleteImage(); resetReviewFormValue(); @@ -53,27 +58,21 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog }: ReviewR const handleSubmit: FormEventHandler = async (event) => { event.preventDefault(); - try { - await uploadToS3(); - await mutateAsync( - { ...reviewFormValue, image: fileUrl }, - { - onSuccess: () => { - resetAndCloseForm(); - scrollToPosition(targetRef); - }, + mutate(formData, { + onSuccess: () => { + resetAndCloseForm(); + scrollToPosition(targetRef); + }, + onError: (error) => { + resetAndCloseForm(); + if (error instanceof Error) { + alert(error.message); + return; } - ); - } catch (error) { - resetAndCloseForm(); - - if (error instanceof Error) { - alert(error.message); - return; - } - alert('리뷰 등록을 다시 시도해주세요'); - } + alert('리뷰 등록을 다시 시도해주세요'); + }, + }); }; return ( diff --git a/frontend/src/contexts/ReviewFormContext.tsx b/frontend/src/contexts/ReviewFormContext.tsx index 0532cb284..34c15659f 100644 --- a/frontend/src/contexts/ReviewFormContext.tsx +++ b/frontend/src/contexts/ReviewFormContext.tsx @@ -2,12 +2,10 @@ import type { PropsWithChildren } from 'react'; import { createContext, useState } from 'react'; import { MIN_DISPLAYED_TAGS_LENGTH } from '@/constants'; -import type { ReviewRequest } from '@/types/review'; - -type ReviewValue = Omit; +import type { ReviewRequest, ReviewRequestKey } from '@/types/review'; interface ReviewFormActionParams { - target: keyof ReviewValue; + target: ReviewRequestKey; value: string | number | boolean; isSelected?: boolean; } @@ -17,14 +15,14 @@ interface ReviewFormAction { resetReviewFormValue: () => void; } -const initialReviewFormValue: ReviewValue = { +const initialReviewFormValue: ReviewRequest = { rating: 0, tagIds: [], content: '', rebuy: false, }; -export const ReviewFormValueContext = createContext(null); +export const ReviewFormValueContext = createContext(null); export const ReviewFormActionContext = createContext(null); const ReviewFormProvider = ({ children }: PropsWithChildren) => { diff --git a/frontend/src/hooks/queries/review/useReviewRegisterFormMutation.ts b/frontend/src/hooks/queries/review/useReviewRegisterFormMutation.ts index cbe7e8d7a..e63cd97c5 100644 --- a/frontend/src/hooks/queries/review/useReviewRegisterFormMutation.ts +++ b/frontend/src/hooks/queries/review/useReviewRegisterFormMutation.ts @@ -1,16 +1,12 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { productApi } from '@/apis'; -import type { ReviewRequest } from '@/types/review'; - -const headers = { 'Content-Type': 'application/json' }; const useReviewRegisterFormMutation = (productId: number) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (data: ReviewRequest) => - productApi.post({ params: `/${productId}/reviews`, credentials: true }, headers, data), + mutationFn: (data: FormData) => productApi.postData({ params: `/${productId}/reviews`, credentials: true }, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product', productId] }), }); }; diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts index fe283d51c..debf6f65e 100644 --- a/frontend/src/types/review.ts +++ b/frontend/src/types/review.ts @@ -24,13 +24,9 @@ export interface ReviewRequest { tagIds: number[]; content: string; rebuy: boolean; - image: string | null; } -export interface ReviewPostRequestBody extends FormData { - image: File; - reviewRequest: ReviewRequest; -} +export type ReviewRequestKey = keyof ReviewRequest; export interface ReviewFavoriteRequestBody { favorite: boolean; From e8360dee6532f562d53446fa18900a9fcf3ec04a Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:19:32 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=ED=8F=BC=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=ED=8F=BC=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/ApiClient.ts | 13 ++++- .../members/useMemberModifyMutation.ts | 9 ++-- frontend/src/pages/MemberModifyPage.tsx | 47 +++++++++---------- frontend/src/types/member.ts | 1 - 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/frontend/src/apis/ApiClient.ts b/frontend/src/apis/ApiClient.ts index c492b5607..26fe6284a 100644 --- a/frontend/src/apis/ApiClient.ts +++ b/frontend/src/apis/ApiClient.ts @@ -37,11 +37,11 @@ export class ApiClient { }); } - postData({ params, queries, credentials = false }: RequestOptions, body?: FormData) { + postData({ params, queries, credentials = false }: RequestOptions, body: FormData) { return fetchApi(this.getUrl(params, queries), { method: 'POST', headers: this.#headers, - body: body ? body : null, + body: body, credentials: credentials ? 'include' : 'omit', }); } @@ -64,6 +64,15 @@ export class ApiClient { }); } + putData({ params, queries, credentials = false }: RequestOptions, body: FormData) { + return fetchApi(this.getUrl(params, queries), { + method: 'PUT', + headers: this.#headers, + body: body, + credentials: credentials ? 'include' : 'omit', + }); + } + delete({ params, queries, credentials = false }: RequestOptions, body?: B) { return fetchApi(this.getUrl(params, queries), { method: 'DELETE', diff --git a/frontend/src/hooks/queries/members/useMemberModifyMutation.ts b/frontend/src/hooks/queries/members/useMemberModifyMutation.ts index e4d0b81ae..f3e039b08 100644 --- a/frontend/src/hooks/queries/members/useMemberModifyMutation.ts +++ b/frontend/src/hooks/queries/members/useMemberModifyMutation.ts @@ -1,19 +1,16 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { memberApi } from '@/apis'; -import type { MemberRequest } from '@/types/member'; -const headers = { 'Content-Type': 'application/json' }; - -const putModifyMember = (body: MemberRequest) => { - return memberApi.put({ credentials: true }, headers, body); +const putModifyMember = (body: FormData) => { + return memberApi.putData({ credentials: true }, body); }; const useMemberModifyMutation = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (body: MemberRequest) => putModifyMember(body), + mutationFn: (body: FormData) => putModifyMember(body), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['member'] }), }); }; diff --git a/frontend/src/pages/MemberModifyPage.tsx b/frontend/src/pages/MemberModifyPage.tsx index 6ecd652a7..0c9477add 100644 --- a/frontend/src/pages/MemberModifyPage.tsx +++ b/frontend/src/pages/MemberModifyPage.tsx @@ -6,20 +6,26 @@ import styled from 'styled-components'; import { Input, SectionTitle, SvgIcon } from '@/components/Common'; import { IMAGE_MAX_SIZE } from '@/constants'; -import { useImageUploader } from '@/hooks/common'; +import { useFormData, useImageUploader } from '@/hooks/common'; import { useMemberModifyMutation, useMemberQuery } from '@/hooks/queries/members'; -import { useS3Upload } from '@/hooks/s3'; +import type { MemberRequest } from '@/types/member'; const MemberModifyPage = () => { const { data: member } = useMemberQuery(); - const { mutateAsync } = useMemberModifyMutation(); + const { mutate } = useMemberModifyMutation(); const { previewImage, imageFile, uploadImage } = useImageUploader(); - const { uploadToS3, fileUrl } = useS3Upload(imageFile); const [nickname, setNickname] = useState(member?.nickname ?? ''); const navigate = useNavigate(); + const formData = useFormData({ + imageKey: 'image', + imageFile: imageFile, + formContentKey: 'memberRequest', + formContent: { nickname }, + }); + if (!member) { return null; } @@ -47,28 +53,19 @@ const MemberModifyPage = () => { const handleSubmit: FormEventHandler = async (event) => { event.preventDefault(); - if (imageFile === null || fileUrl === null) { - return; - } - - try { - await uploadToS3(); - await mutateAsync( - { nickname, image: fileUrl }, - { - onSuccess: () => { - navigate('/members'); - }, + mutate(formData, { + onSuccess: () => { + navigate('/members'); + }, + onError: (error) => { + if (error instanceof Error) { + alert(error.message); + return; } - ); - } catch (error) { - if (error instanceof Error) { - alert(error.message); - return; - } - - alert('회원정보 수정을 다시 시도해주세요.'); - } + + alert('회원정보 수정을 다시 시도해주세요.'); + }, + }); }; return ( diff --git a/frontend/src/types/member.ts b/frontend/src/types/member.ts index f46459e16..83a294576 100644 --- a/frontend/src/types/member.ts +++ b/frontend/src/types/member.ts @@ -5,5 +5,4 @@ export interface Member { export interface MemberRequest { nickname: string; - image: string; } From 70fbc003cf87965cc53fcf8718bc6a33da49b60c Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:21:18 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20s3=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/index.ts | 1 - frontend/src/hooks/queries/s3/index.ts | 1 - .../hooks/queries/s3/usePresignedMutation.ts | 18 -------- frontend/src/hooks/s3/index.ts | 1 - frontend/src/hooks/s3/useS3Upload.ts | 44 ------------------- frontend/src/types/s3.ts | 7 --- 6 files changed, 72 deletions(-) delete mode 100644 frontend/src/hooks/queries/s3/index.ts delete mode 100644 frontend/src/hooks/queries/s3/usePresignedMutation.ts delete mode 100644 frontend/src/hooks/s3/index.ts delete mode 100644 frontend/src/hooks/s3/useS3Upload.ts delete mode 100644 frontend/src/types/s3.ts diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts index d0bab9489..73d9e4928 100644 --- a/frontend/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -9,4 +9,3 @@ export const memberApi = new ApiClient('/members'); export const recipeApi = new ApiClient('/recipes'); export const searchApi = new ApiClient('/search'); export const logoutApi = new ApiClient('/logout'); -export const s3Api = new ApiClient('/s3'); diff --git a/frontend/src/hooks/queries/s3/index.ts b/frontend/src/hooks/queries/s3/index.ts deleted file mode 100644 index f9e8aadb2..000000000 --- a/frontend/src/hooks/queries/s3/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as usePresignedMutation } from './usePresignedMutation'; diff --git a/frontend/src/hooks/queries/s3/usePresignedMutation.ts b/frontend/src/hooks/queries/s3/usePresignedMutation.ts deleted file mode 100644 index fa16a17df..000000000 --- a/frontend/src/hooks/queries/s3/usePresignedMutation.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { s3Api } from '@/apis'; -import type { PresignedRequest } from '@/types/s3'; - -const headers = { 'Content-Type': 'application/json' }; - -const postPresigned = (body: PresignedRequest) => { - return s3Api.post({ params: '/presigned' }, headers, body); -}; - -const usePresignedMutation = () => { - return useMutation({ - mutationFn: (body: PresignedRequest) => postPresigned(body), - }); -}; - -export default usePresignedMutation; diff --git a/frontend/src/hooks/s3/index.ts b/frontend/src/hooks/s3/index.ts deleted file mode 100644 index c6f94a53d..000000000 --- a/frontend/src/hooks/s3/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useS3Upload } from './useS3Upload'; diff --git a/frontend/src/hooks/s3/useS3Upload.ts b/frontend/src/hooks/s3/useS3Upload.ts deleted file mode 100644 index 981ad3ef6..000000000 --- a/frontend/src/hooks/s3/useS3Upload.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { usePresignedMutation } from '../queries/s3'; - -import type { PresignedResponse } from '@/types/s3'; - -const ENVIRONMENT = window.location.href.includes('dev') ? 'dev' : 'prod'; -const UPLOAD_ERROR_MESSAGE = '이미지 업로드에 실패했습니다. 다시 시도해주세요.'; - -const putFileToS3 = (url: string, file: File) => { - fetch(url, { - method: 'PUT', - body: file, - headers: { - 'Content-Type': file.type, - }, - }); -}; - -const useS3Upload = (file: File | null) => { - const { mutateAsync } = usePresignedMutation(); - - const getPreSignedUrl = async (file: File) => { - const response = await mutateAsync({ fileName: file.name }); - const { preSignedUrl }: PresignedResponse = await response.json(); - - return preSignedUrl; - }; - - const fileUrl = file !== null ? `${process.env.CLOUDFRONT_URL}/${ENVIRONMENT}/${file.name}` : null; - - const uploadToS3 = async () => { - try { - if (file !== null) { - const preSignedUrl = await getPreSignedUrl(file); - putFileToS3(preSignedUrl, file); - } - } catch { - throw new Error(UPLOAD_ERROR_MESSAGE); - } - }; - - return { uploadToS3, fileUrl }; -}; - -export default useS3Upload; diff --git a/frontend/src/types/s3.ts b/frontend/src/types/s3.ts deleted file mode 100644 index a46a1b10c..000000000 --- a/frontend/src/types/s3.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface PresignedRequest { - fileName: string; -} - -export interface PresignedResponse { - preSignedUrl: string; -} From b8fcc3ea08a60ac93e6d0e255430ad2b1e00be5e Mon Sep 17 00:00:00 2001 From: Leejin-Yang Date: Tue, 12 Sep 2023 17:47:34 +0900 Subject: [PATCH 7/7] =?UTF-8?q?test:=20useImageUploader=20=ED=9B=85=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/hooks/useImageUploader.test.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 frontend/__tests__/hooks/useImageUploader.test.ts diff --git a/frontend/__tests__/hooks/useImageUploader.test.ts b/frontend/__tests__/hooks/useImageUploader.test.ts new file mode 100644 index 000000000..3de2da051 --- /dev/null +++ b/frontend/__tests__/hooks/useImageUploader.test.ts @@ -0,0 +1,61 @@ +import { useImageUploader } from '@/hooks/common'; +import { renderHook, act } from '@testing-library/react'; + +const originalCreateObjectUrl = URL.createObjectURL; +const originalRevokeObjectUrl = URL.revokeObjectURL; + +beforeAll(() => { + URL.createObjectURL = jest.fn(() => 'mocked url'); + URL.revokeObjectURL = jest.fn(); +}); + +afterAll(() => { + URL.createObjectURL = originalCreateObjectUrl; + URL.revokeObjectURL = originalRevokeObjectUrl; +}); + +it('uploadImage를 사용하여 이미지 파일을 업로드할 수 있다.', () => { + const { result } = renderHook(() => useImageUploader()); + + const file = new File(['dummy content'], 'example.png', { type: 'image/png' }); + + act(() => { + result.current.uploadImage(file); + }); + + expect(result.current.imageFile).toBe(file); + expect(result.current.previewImage).toBe('mocked url'); + expect(URL.createObjectURL).toHaveBeenCalledWith(file); +}); + +it('이미지 파일이 아니면 "이미지 파일만 업로드 가능합니다." 메시지를 보여주는 alert 창이 뜬다.', () => { + const { result } = renderHook(() => useImageUploader()); + + const file = new File(['dummy content'], 'example.txt', { type: 'text/plain' }); + + global.alert = jest.fn(); + + act(() => { + result.current.uploadImage(file); + }); + + expect(global.alert).toHaveBeenCalledWith('이미지 파일만 업로드 가능합니다.'); +}); + +it('deleteImage를 사용하여 이미지 파일을 삭제할 수 있다.', () => { + const { result } = renderHook(() => useImageUploader()); + + const file = new File(['dummy content'], 'example.png', { type: 'image/png' }); + + act(() => { + result.current.uploadImage(file); + }); + + act(() => { + result.current.deleteImage(); + }); + + expect(result.current.imageFile).toBeNull(); + expect(result.current.previewImage).toBe(''); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('mocked url'); +});