diff --git a/src/api/editwine.ts b/src/api/editwine.ts new file mode 100644 index 00000000..dd1b2bc4 --- /dev/null +++ b/src/api/editwine.ts @@ -0,0 +1,31 @@ +import apiClient from '@/api/apiClient'; + +export interface UpdateWineRequest { + wineId: number; + name: string; + region: string; + image: string; + price: number; + avgRating: number; + type: 'RED' | 'WHITE' | 'SPARKLING'; +} + +export const uploadImage = async (file: File): Promise => { + const formData = new FormData(); + formData.append('image', file); + + const response = (await apiClient.post<{ url: string }>( + `/${process.env.NEXT_PUBLIC_TEAM}/images/upload`, + formData, + { + headers: { 'Content-Type': 'multipart/form-data' }, + }, + )) as unknown as { url: string }; //타입스크립트가 제안하는 대로 중간에 unknown 타입으로 한 번 변환한 후, 원하는 최종 타입으로 다시 변환하는 "이중 캐스팅 + console.log(response); + //apiClient가 res.data만 반환함 + return response.url; +}; + +export const updateWine = async ({ wineId, ...body }: UpdateWineRequest) => { + return apiClient.patch(`/${process.env.NEXT_PUBLIC_TEAM}/wines/${wineId}/`, body); +}; diff --git a/src/components/Modal/ReviewModal/AddReviewModal.tsx b/src/components/Modal/ReviewModal/AddReviewModal.tsx index a947fa61..25c0a29a 100644 --- a/src/components/Modal/ReviewModal/AddReviewModal.tsx +++ b/src/components/Modal/ReviewModal/AddReviewModal.tsx @@ -113,6 +113,7 @@ const AddReviewModal = ({ wineId, wineName }: { wineId: number; wineName: string onSuccess: (data) => { console.log('리뷰 등록 성공', data); queryClient.invalidateQueries({ queryKey: ['reviews'] }); + queryClient.invalidateQueries({ queryKey: ['wineDetail'] }); reset(); setShowRegisterModal(false); }, diff --git a/src/components/Modal/WineModal/EditWineModal.tsx b/src/components/Modal/WineModal/EditWineModal.tsx new file mode 100644 index 00000000..d9f2c271 --- /dev/null +++ b/src/components/Modal/WineModal/EditWineModal.tsx @@ -0,0 +1,279 @@ +import React, { useRef, useState } from 'react'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; + +import { updateWine, uploadImage } from '@/api/editwine'; +import SelectDropdown from '@/components/common/dropdown/SelectDropdown'; +import Input from '@/components/common/Input'; +import BasicModal from '@/components/common/Modal/BasicModal'; +import { Button } from '@/components/ui/button'; + +interface WineForm { + wineName: string; + winePrice: number; + wineOrigin: string; + wineImage: FileList; + wineType: string; +} +interface WineData { + wineId: number; + name: string; + price: number; + region: string; + image: string; + type: 'RED' | 'WHITE' | 'SPARKLING'; + avgRating: number; +} + +interface EditWineModalProps { + wine: WineData; + showEditModal: boolean; + setShowEditModal: (state: boolean) => void; +} + +const EditWineModal = ({ wine, showEditModal, setShowEditModal }: EditWineModalProps) => { + const [previewImage, setPreviewImage] = useState(wine.image); + const fileInputRef = useRef(null); + const queryClient = useQueryClient(); + + const { + register, + handleSubmit, + formState: { errors }, + clearErrors, + reset, + setValue, + trigger, + watch, + } = useForm({ + defaultValues: { + wineName: wine.name, + winePrice: wine.price, + wineOrigin: wine.region, + wineType: wine.type, + }, + }); + + const category = watch('wineType'); + const selectedCategoryLabel = { + RED: 'Red', + WHITE: 'White', + SPARKLING: 'Sparkling', + }[category]; + + const categoryOptions = [ + { label: 'Red', value: 'RED' }, + { label: 'White', value: 'WHITE' }, + { label: 'Sparkling', value: 'SPARKLING' }, + ]; + + const triggerFileSelect = () => fileInputRef.current?.click(); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setPreviewImage(URL.createObjectURL(file)); + } + }; + + const updateWineMutation = useMutation({ + mutationFn: updateWine, + onSuccess: () => { + console.log('와인수정완료'); + queryClient.invalidateQueries({ queryKey: ['wines'] }); + setShowEditModal(false); + }, + onError: (error) => { + console.log('와인수정실패', error); + }, + }); + + const onSubmit = async (form: WineForm) => { + try { + const file = form.wineImage?.[0]; + const imageUrl = file ? await uploadImage(file) : wine.image; + + updateWineMutation.mutate({ + wineId: wine.wineId, + name: form.wineName, + price: Number(form.winePrice), + region: form.wineOrigin, + type: form.wineType.toUpperCase() as 'RED' | 'WHITE' | 'SPARKLING', + image: imageUrl, + avgRating: wine.avgRating, + }); + } catch (error) { + console.log('와인수정실패', error); + } + }; + + const closeModalReset = (isOpen: boolean) => { + setShowEditModal(isOpen); + if (!isOpen) { + reset({ + wineName: wine.name, + winePrice: wine.price, + wineOrigin: wine.region, + wineType: wine.type, + }); + setPreviewImage(wine.image); + } + }; + + return ( +
+ + 수정 완료 + + } + > +
+ {/* 와인 이름 */} +

+ 와인 이름 +

+ clearErrors('wineName'), + })} + errorMessage={errors.wineName?.message} + /> + + {/* 가격 */} +

+ 가격 +

+ clearErrors('winePrice'), + })} + errorMessage={errors.winePrice?.message} + /> + + {/* 원산지 */} +

+ 원산지 +

+ clearErrors('wineOrigin'), + })} + errorMessage={errors.wineOrigin?.message} + /> + + {/* 타입 */} +

+ 타입 +

+ { + setValue('wineType', value); + trigger('wineType'); + }} + placeholder='타입 선택' + trigger={ + + } + /> + {errors.wineType?.message && ( +
+

{errors.wineType.message}

+
+ )} + clearErrors('wineType'), + })} + /> + + {/* 와인 사진 */} +

+ 와인 사진 +

+ { + clearErrors('wineImage'); + handleImageChange(e); + }, + })} + ref={(e) => { + register('wineImage').ref(e); + fileInputRef.current = e; + }} + /> +
+
+ {previewImage ? ( + // eslint-disable-next-line @next/next/no-img-element + 미리보기 + ) : ( +
+ {/* */} +
+ )} +
+ {errors.wineImage?.message && ( +
+

{errors.wineImage.message}

+
+ )} +
+ +
+
+ ); +}; +export default EditWineModal;