Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions src/api/editwine.ts
Original file line number Diff line number Diff line change
@@ -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<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 updateWine = async ({ wineId, ...body }: UpdateWineRequest) => {
return apiClient.patch(`/${process.env.NEXT_PUBLIC_TEAM}/wines/${wineId}/`, body);
};
1 change: 1 addition & 0 deletions src/components/Modal/ReviewModal/AddReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
279 changes: 279 additions & 0 deletions src/components/Modal/WineModal/EditWineModal.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(wine.image);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const queryClient = useQueryClient();

const {
register,
handleSubmit,
formState: { errors },
clearErrors,
reset,
setValue,
trigger,
watch,
} = useForm<WineForm>({
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<HTMLInputElement>) => {
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 (
<div>
<BasicModal
type='register'
title='๋‚ด๊ฐ€ ๋“ฑ๋กํ•œ ์™€์ธ'
open={showEditModal}
onOpenChange={closeModalReset}
buttons={
<Button
onClick={handleSubmit(onSubmit)}
type='submit'
variant='purpleDark'
size='xl'
width='full'
fontSize='lg'
>
์ˆ˜์ • ์™„๋ฃŒ
</Button>
}
>
<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'
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'
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'
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) => {
setValue('wineType', value);
trigger('wineType');
}}
placeholder='ํƒ€์ž… ์„ ํƒ'
trigger={
<button
type='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 mt-1'>{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
id='wineImage'
type='file'
accept='image/*'
className='custom-text-md-regular md:custom-text-lg-regular hidden'
{...register('wineImage', {
onChange: (e) => {
clearErrors('wineImage');
handleImageChange(e);
},
})}
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={triggerFileSelect}
>
{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 EditWineModal;