diff --git a/src/api/addwine.ts b/src/api/addwine.ts new file mode 100644 index 0000000..4b5d895 --- /dev/null +++ b/src/api/addwine.ts @@ -0,0 +1,30 @@ +import apiClient from '@/api/apiClient'; + +export interface PostWineRequest { + name: string; + region: string; + image: string; + price: 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 postWine = async (data: PostWineRequest) => { + const response = await apiClient.post(`/${process.env.NEXT_PUBLIC_TEAM}/wines`, data); + return response.data; +}; diff --git a/src/assets/camera.svg b/src/assets/camera.svg new file mode 100644 index 0000000..fa3bd18 --- /dev/null +++ b/src/assets/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/dropdowntriangle.svg b/src/assets/dropdowntriangle.svg new file mode 100644 index 0000000..4a12945 --- /dev/null +++ b/src/assets/dropdowntriangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Modal/WineModal/AddWineModal.tsx b/src/components/Modal/WineModal/AddWineModal.tsx new file mode 100644 index 0000000..ffcce06 --- /dev/null +++ b/src/components/Modal/WineModal/AddWineModal.tsx @@ -0,0 +1,275 @@ +import React, { useRef, useState } from 'react'; + +import { useForm } from 'react-hook-form'; + +import { uploadImage, postWine, PostWineRequest } from '@/api/addwine'; +import CameraIcon from '@/assets/camera.svg'; +import DropdownIcon from '@/assets/dropdowntriangle.svg'; + +import SelectDropdown from '../../common/dropdown/SelectDropdown'; +import Input from '../../common/Input'; +import BasicModal from '../../common/Modal/BasicModal'; +import { Button } from '../../ui/button'; + +interface WineForm { + wineName: string; + winePrice: number; + wineOrigin: string; + wineImage: FileList; + wineType: string; +} + +const AddWineModal = () => { + const [showRegisterModal, setShowRegisterModal] = useState(false); + const [category, setCategory] = useState(''); + const fileInputRef = useRef(null); + const [previewImage, setPreviewImage] = useState(null); + + const trigerFileSelect = () => { + fileInputRef.current?.click(); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const imageUrl = URL.createObjectURL(file); + setPreviewImage(imageUrl); + } else { + setPreviewImage(null); + } + }; + + const { + register, + handleSubmit, + formState: { errors }, + clearErrors, + trigger, + setValue, + reset, + } = useForm({ + mode: 'onBlur', + }); + + const resetForm = () => { + reset({ + wineName: '', + winePrice: NaN, + wineOrigin: '', + wineImage: {} as FileList, + wineType: '', + }); + setCategory(''); + setPreviewImage(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + }; + + const onSubmit = async (form: WineForm) => { + try { + const file = form.wineImage[0]; + const imageUrl = await uploadImage(file); + const requestData: PostWineRequest = { + name: form.wineName, + region: form.wineOrigin, + image: imageUrl, + price: Number(form.winePrice), + type: form.wineType.toUpperCase() as 'RED' | 'WHITE' | 'SPARKLING', + }; + await postWine(requestData); + + console.log('와인등록완료'); + resetForm(); + setShowRegisterModal(false); + } catch (error) { + console.error('와인등록실패', error); + alert('와인등록실패'); + } + }; + + const categoryOptions = [ + { label: 'Red', value: 'Red' }, + { label: 'White', value: 'White' }, + { label: 'Sparkling', value: 'Sparkling' }, + ]; + + const selectedCategoryLabel = categoryOptions.find((opt) => opt.value === category)?.label; + + //모달창 끄면 리셋되게 + const closeModalReset = (isOpen: boolean) => { + setShowRegisterModal(isOpen); + if (!isOpen) { + setTimeout(() => { + resetForm(); + }, 50); + } + }; + //// + return ( +
+ + + + +
+ } + > +
+

+ 와인 이름 +

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

+ 가격 +

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

+ 원산지 +

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

+ 타입 +

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

{errors.wineType.message}

+
+ )} + clearErrors('wineType'), + })} + /> +

+ 와인 사진 +

+ { + clearErrors('wineImage'); + handleImageChange(e); + console.log(e.target.files?.[0]); + }, + })} + 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 AddWineModal;