-
Notifications
You must be signed in to change notification settings - Fork 5
Feature/addwine modal #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
aa794a5
b44044c
10d7494
488b4c7
1bf82a7
980a7a3
3145bc9
e025b4e
25339ce
b84ac4b
7822734
c403107
085737f
95ed37a
5c49ff4
38cf640
06e0b8d
18b57cb
619150b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string> => { | ||
| 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| 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<HTMLInputElement | null>(null); | ||
| const [previewImage, setPreviewImage] = useState<string | null>(null); | ||
|
|
||
| const trigerFileSelect = () => { | ||
| fileInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| 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<WineForm>({ | ||
| mode: 'onBlur', | ||
| }); | ||
|
|
||
| const onSubmit = async (form: WineForm) => { | ||
| try { | ||
| const file = form.wineImage[0]; | ||
| console.log('1'); | ||
| const imageUrl = await uploadImage(file); | ||
| console.log('2'); | ||
| const requestData: PostWineRequest = { | ||
| name: form.wineName, | ||
| region: form.wineOrigin, | ||
| image: imageUrl, | ||
| price: Number(form.winePrice), | ||
| type: form.wineType.toUpperCase() as 'RED' | 'WHITE' | 'SPARKLING', | ||
| }; | ||
| console.log('3'); | ||
| await postWine(requestData); | ||
|
|
||
| console.log('์์ธ๋ฑ๋ก์๋ฃ'); | ||
| reset({ | ||
| wineName: '', | ||
| winePrice: NaN, | ||
| wineOrigin: '', | ||
| wineImage: {} as FileList, | ||
| wineType: '', | ||
| }); | ||
| setPreviewImage(null); | ||
| fileInputRef.current && (fileInputRef.current.value = ''); | ||
| setCategory(''); | ||
| 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(() => { | ||
| reset({ | ||
| wineName: '', | ||
| winePrice: NaN, | ||
| wineOrigin: '', | ||
| wineImage: {} as FileList, | ||
| wineType: '', | ||
| }); | ||
| setCategory(''); | ||
| setPreviewImage(null); //์ด๋ฏธ์ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๊ธฐํ | ||
| fileInputRef.current && (fileInputRef.current.value = ''); | ||
| }, 50); | ||
| } | ||
| }; | ||
| //// | ||
| return ( | ||
| <div> | ||
| <Button variant='purpleDark' size='lg' width='lg' onClick={() => setShowRegisterModal(true)}> | ||
| ์์ธ ๋ฑ๋กํ๊ธฐ | ||
| </Button> | ||
| <BasicModal | ||
| type='register' | ||
| title='์์ธ ๋ฑ๋ก' | ||
| open={showRegisterModal} | ||
| onOpenChange={closeModalReset} | ||
| showCloseButton={false} | ||
| buttons={ | ||
| <div className='flex gap-2'> | ||
| <Button | ||
| onClick={() => { | ||
| reset({ | ||
|
||
| wineName: '', | ||
| winePrice: NaN, | ||
| wineOrigin: '', | ||
| wineImage: {} as FileList, | ||
| wineType: '', | ||
| }); | ||
| setCategory(''); | ||
| setPreviewImage(null); | ||
| fileInputRef.current && (fileInputRef.current.value = ''); | ||
| setShowRegisterModal(false); | ||
| }} | ||
| variant='purpleLight' | ||
| size='xl' | ||
| className='w-[96px] md:w-[108px]' | ||
| fontSize='lg' | ||
| > | ||
| ์ทจ์ | ||
| </Button> | ||
| <Button | ||
| onClick={handleSubmit(onSubmit)} | ||
| type='submit' | ||
| variant='purpleDark' | ||
| size='xl' | ||
| className='w-[223px] md:w-[294px]' | ||
| fontSize='lg' | ||
| > | ||
| ์์ธ ๋ฑ๋กํ๊ธฐ | ||
| </Button> | ||
| </div> | ||
| } | ||
| > | ||
| <form onSubmit={handleSubmit(onSubmit)} encType='multipart/form-data'> | ||
| <p className='custom-text-md-medium md:custom-text-lg-medium mb-[10px] md:mb-[12px] mt-[22px] md:mt-[24px] '> | ||
| ์์ธ ์ด๋ฆ | ||
| </p> | ||
| <Input | ||
| className='custom-text-md-regular md:custom-text-lg-regular' | ||
| id='wineName' | ||
| type='text' | ||
| variant='name' | ||
| placeholder='์์ธ ์ด๋ฆ ์ ๋ ฅ' | ||
| {...register('wineName', { | ||
| required: '์์ธ ์ด๋ฆ์ ์ ๋ ฅํด ์ฃผ์ธ์.', | ||
| onChange: () => clearErrors('wineName'), | ||
| })} | ||
| errorMessage={errors.wineName?.message} | ||
| /> | ||
| <p className='custom-text-md-medium md:custom-text-lg-medium mb-[10px] md:mb-[12px] mt-[22px] md:mt-[24px]'> | ||
| ๊ฐ๊ฒฉ | ||
| </p> | ||
| <Input | ||
| className='[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none custom-text-md-regular md:custom-text-lg-regular' | ||
| id='winePrice' | ||
| type='number' | ||
| variant='name' | ||
| placeholder='๊ฐ๊ฒฉ ์ ๋ ฅ' | ||
| {...register('winePrice', { | ||
| required: '๊ฐ๊ฒฉ์ ์ ๋ ฅํด ์ฃผ์ธ์.', | ||
| onChange: () => clearErrors('winePrice'), | ||
| })} | ||
| errorMessage={errors.winePrice?.message} | ||
| /> | ||
| <p className='custom-text-md-medium md:custom-text-lg-medium mb-[10px] md:mb-[12px] mt-[22px] md:mt-[24px]'> | ||
| ์์ฐ์ง | ||
| </p> | ||
| <Input | ||
| className='custom-text-md-regular md:custom-text-lg-regular' | ||
| id='wineOrigin' | ||
| type='text' | ||
| variant='name' | ||
| placeholder='์์ฐ์ง ์ ๋ ฅ' | ||
| {...register('wineOrigin', { | ||
| required: '์์ฐ์ง๋ฅผ ์ ๋ ฅํด ์ฃผ์ธ์.', | ||
| onChange: () => clearErrors('wineOrigin'), | ||
| })} | ||
| errorMessage={errors.wineOrigin?.message} | ||
| /> | ||
| <p className='custom-text-md-medium md:custom-text-lg-medium mb-[10px] md:mb-[12px] mt-[22px] md:mt-[24px]'> | ||
| ํ์ | ||
| </p> | ||
| <SelectDropdown | ||
| selectedValue={category} | ||
| options={categoryOptions} | ||
| onChange={(value) => { | ||
| setCategory(value); | ||
| setValue('wineType', value, { shouldValidate: true }); | ||
| trigger('wineType'); | ||
| }} | ||
| placeholder='ํ์ ์ ํ' | ||
| trigger={ | ||
| <button | ||
| className={`w-full h-[42px] md:h-[48px] px-4 py-2 border rounded-[12px] md:rounded-[16px] text-left ${category ? 'text-black' : 'text-gray-500'}`} | ||
| > | ||
| <> | ||
| <span>{selectedCategoryLabel || 'ํ์ ์ ํ'}</span> | ||
| <DropdownIcon className='ml-2 w-4 h-4 bg-black' /> | ||
| </> | ||
| </button> | ||
| } | ||
| /> | ||
| {errors.wineType?.message && ( | ||
| <div className='relative'> | ||
| <p className='text-red-500 absolute '>{errors.wineType.message}</p> | ||
| </div> | ||
| )} | ||
| <Input | ||
| id='wineType' | ||
| type='text' | ||
| className='custom-text-md-regular md:custom-text-lg-regular hidden ' | ||
| {...register('wineType', { | ||
| required: 'ํ์ ์ ์ ํํด ์ฃผ์ธ์.', | ||
| onChange: () => clearErrors('wineType'), | ||
| })} | ||
| /> | ||
| <p className='custom-text-md-medium md:custom-text-lg-medium mt-[24px] md:mt-[26px]'> | ||
| ์์ธ ์ฌ์ง | ||
| </p> | ||
| <Input | ||
| className='custom-text-md-regular md:custom-text-lg-regular hidden ' | ||
| id='wineImage' | ||
| type='file' | ||
| accept='image/*' | ||
| {...register('wineImage', { | ||
| required: '์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํด ์ฃผ์ธ์.', | ||
| onChange: (e) => { | ||
| clearErrors('wineImage'); | ||
| handleImageChange(e); | ||
| console.log(e.target.files?.[0]); | ||
| }, | ||
| })} | ||
| ref={(e) => { | ||
| register('wineImage').ref(e); | ||
| fileInputRef.current = e; | ||
| }} | ||
| /> | ||
| <div className='mt-2 mb-5'> | ||
| <div | ||
| className='w-[140px] aspect-square bg-gray-100 rounded-2xl overflow-hidden flex items-center justify-center relative cursor-pointer' | ||
| onClick={trigerFileSelect} | ||
| > | ||
| {previewImage ? ( | ||
| // eslint-disable-next-line @next/next/no-img-element | ||
| <img src={previewImage} alt='๋ฏธ๋ฆฌ๋ณด๊ธฐ' className='w-full h-full object-cover' /> | ||
| ) : ( | ||
| <div className='flex flex-col items-center text-gray-400'> | ||
| <CameraIcon className='w-6 h-6 mb-2' /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| {errors.wineImage?.message && ( | ||
| <div className='relative'> | ||
| <p className='text-red-500 absolute mt-1'>{errors.wineImage.message}</p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </form> | ||
| </BasicModal> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default AddWineModal; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ด ๋ถ๋ถ ์ฝ๋๊ฐ ์์ฃผ ๋ฐ๋ณต๋๋ ๊ฒ ๊ฐ์์ ๋ฉ์๋๋ก ๋นผ์ ์ฌ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค!