Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aa794a5
Merge : dev์— ๋จธ์ง€
llmojoll Jul 22, 2025
b44044c
Feat : ๋ชจ๋‹ฌ์ฐฝ ๋„์šฐ๊ธฐ
llmojoll Jul 22, 2025
10d7494
Feat : ์™€์ธ๋“ฑ๋กํ•˜๊ธฐ ๋ชจ๋‹ฌ UI๊ตฌํ˜„, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ, ์ž…๋ ฅ์ดˆ๊ธฐํ™” ๊ตฌํ˜„
llmojoll Jul 22, 2025
488b4c7
Styled : ํฐํŠธ, margin ์ˆ˜์ •
llmojoll Jul 22, 2025
1bf82a7
Feat : api ์—ฐ๊ฒฐ์ค‘
llmojoll Jul 22, 2025
980a7a3
Style : ๋ฒ„ํŠผ ์Šคํƒ€์ผ ์ˆ˜์ •
llmojoll Jul 23, 2025
3145bc9
Feat : ๋ชจ๋‹ฌ์ฐฝ ๋ฐ–์— ๋ˆ„๋ฅด๋ฉด ๋ฆฌ์…‹๋˜๊ฒŒ ๊ตฌํ˜„
llmojoll Jul 23, 2025
e025b4e
Chore : index์—์„œ ๋ชจ๋‹ฌ ๋ฒ„ํŠผ ์‚ญ์ œ
llmojoll Jul 23, 2025
25339ce
Merge branch 'dev' into feature/addwine-modal
llmojoll Jul 23, 2025
b84ac4b
Fix : QA ํ”ผ๋“œ๋ฐฑ๋ฐ˜์˜ ๋ฒ„ํŠผ ์ƒ‰์ƒ์ˆ˜์ •, X๋ฒ„ํŠผ ์‚ญ์ œ, ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ถ”๊ฐ€
llmojoll Jul 24, 2025
7822734
Merge branch 'dev' into feature/addwine-modal
llmojoll Jul 24, 2025
c403107
Merge branch 'dev' into feature/addwine-modal
llmojoll Jul 25, 2025
085737f
Feat : ์—๋Ÿฌ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ๋ฐ ์œ„์น˜ ์กฐ์ •
llmojoll Jul 26, 2025
95ed37a
Feat : api ์—ฐ๊ฒฐ, ํ…Œ์ŠคํŠธ ์™„
llmojoll Jul 26, 2025
5c49ff4
Merge branch 'dev' into feature/addwine-modal
llmojoll Jul 26, 2025
38cf640
Refactor : apiํŒŒ์ผ ๋”ฐ๋กœ ์ƒ์„ฑํ•ด์„œ ์—ฐ๊ฒฐ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‚ญ์ œ
llmojoll Jul 26, 2025
06e0b8d
Merge branch 'dev' into feature/addwine-modal
llmojoll Jul 27, 2025
18b57cb
Fix : props ์ด๋ฆ„ ๋ณ€๊ฒฝ
llmojoll Jul 27, 2025
619150b
Refactor : ์˜คํƒ€์ˆ˜์ •, ์ค‘๋ณต๋กœ์ง ํ•จ์ˆ˜๋กœ ์ฒ˜๋ฆฌ
llmojoll Jul 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/api/addwine.ts
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;
};
3 changes: 3 additions & 0 deletions src/assets/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/dropdowntriangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
292 changes: 292 additions & 0 deletions src/components/Modal/WineModal/AddWineModal.tsx
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('');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ด ๋ถ€๋ถ„ ์ฝ”๋“œ๊ฐ€ ์ž์ฃผ ๋ฐ˜๋ณต๋˜๋Š” ๊ฒƒ ๊ฐ™์•„์„œ ๋ฉ”์†Œ๋“œ๋กœ ๋นผ์„œ ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค!

setShowRegisterModal(false);
} catch (error) {
console.error('์™€์ธ๋“ฑ๋ก์‹คํŒจ', error);
alert('์™€์ธ๋“ฑใ„น๊ณก์‹คํŒจ');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์˜คํƒ€ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค!

}
};

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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๋ฆฌํŒฉํ† ๋ง ํ•˜์‹ ๋‹ค๊ณ  ํ•˜์…จ๋Š”๋ฐ ํ•˜์‹ค ๋•Œ ์ด๋Ÿฐ ์ค‘๋ณต๋˜๋Š” 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;