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
28 changes: 28 additions & 0 deletions src/api/editreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import apiClient from '@/api/apiClient';

interface UpdateReviewRequest {
reviewId: number;
rating: number;
lightBold: number;
smoothTannic: number;
drySweet: number;
softAcidic: number;
aroma: string[];
content: string;
}

interface UpdateReviewResponse {
success: boolean;
message?: string;
}

export const updateReview = async ({
reviewId,
...body
}: UpdateReviewRequest): Promise<UpdateReviewResponse> => {
const response = await apiClient.patch<UpdateReviewResponse>(
`/${process.env.NEXT_PUBLIC_TEAM}/reviews/${reviewId}`,
body,
);
return response.data;
};
305 changes: 305 additions & 0 deletions src/components/Modal/ReviewModal/EditReviewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import React, { useState } from 'react';

import { useMutation, useQueryClient } from '@tanstack/react-query';
import Image from 'next/image';
import { useForm } from 'react-hook-form';

import { updateReview } from '@/api/editreview';
import BasicModal from '@/components/common/Modal/BasicModal';
import StarRating from '@/components/Modal/ReviewModal/StarRating';
import { cn } from '@/lib/utils';

import FlavorSlider from '../../common/slider/FlavorSlider';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';

interface ReviewForm {
rating: number;
sliderLightBold: number;
sliderSmoothTanic: number;
sliderdrySweet: number;
slidersoftAcidic: number;
aroma: Array<string>;
content: string;
}

interface ReviewData {
reviewId: number;
rating: number;
sliderLightBold: number;
sliderSmoothTanic: number;
sliderdrySweet: number;
slidersoftAcidic: number;
aroma: string[];
content: string;
}

const aromaOptions = [
'์ฒด๋ฆฌ',
'๋ฒ ๋ฆฌ',
'์˜คํฌ',
'๋ฐ”๋‹๋ผ',
'ํ›„์ถ”',
'์ œ๋นต',
'ํ’€',
'์‚ฌ๊ณผ',
'๋ณต์ˆญ์•„',
'์‹œํŠธ๋Ÿฌ์Šค',
'ํŠธ๋กœํ”ผ์ปฌ',
'๋ฏธ๋„ค๋ž„',
'๊ฝƒ',
'๋‹ด๋ฑƒ์žŽ',
'ํ™',
'์ดˆ์ฝœ๋ฆฟ',
'์ŠคํŒŒ์ด์Šค',
'์นด๋ผ๋ฉœ',
'๊ฐ€์ฃฝ',
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

as const๋ฅผ ์“ฐ๊ณ  ๋‹ค๋ฅธ ๋ถ€๋ถ„๋“ค๋„ typeof aromaOptions์„ ์“ฐ๋ฉด ๋” ์—„๊ฒฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์€๋ฐ
ํฌ๋ฆฌํ‹ฐ์ปฌ ํ•˜์ง€ ์•Š์œผ๋‹ˆ ๋‹ค์Œ์— ์ƒ๊ฐํ•ด๋ณด์…”๋„ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค :)


const aromaMap: Record<string, string> = {
์ฒด๋ฆฌ: 'CHERRY',
๋ฒ ๋ฆฌ: 'BERRY',
์˜คํฌ: 'OAK',
๋ฐ”๋‹๋ผ: 'VANILLA',
ํ›„์ถ”: 'PEPPER',
์ œ๋นต: 'BAKERY',
ํ’€: 'GRASS',
์‚ฌ๊ณผ: 'APPLE',
๋ณต์ˆญ์•„: 'PEACH',
์‹œํŠธ๋Ÿฌ์Šค: 'CITRUS',
ํŠธ๋กœํ”ผ์ปฌ: 'TROPICAL',
๋ฏธ๋„ค๋ž„: 'MINERAL',
๊ฝƒ: 'FLOWER',
๋‹ด๋ฑƒ์žŽ: 'TOBACCO',
ํ™: 'EARTH',
์ดˆ์ฝœ๋ฆฟ: 'CHOCOLATE',
์ŠคํŒŒ์ด์Šค: 'SPICE',
์นด๋ผ๋ฉœ: 'CARAMEL',
๊ฐ€์ฃฝ: 'LEATHER',
};

const EditReviewModal = ({
wineName,
reviewData,
}: {
wineName: string;
reviewData: ReviewData;
}) => {
const [showEditModal, setShowEditModal] = useState(false);
const queryClient = useQueryClient();

const updateReviewMutation = useMutation({
mutationFn: updateReview,
onSuccess: () => {
console.log('๋ฆฌ๋ทฐ ์ˆ˜์ • ์™„๋ฃŒ');
queryClient.invalidateQueries({ queryKey: ['reviews'] });
setShowEditModal(false);
},
onError: (error) => {
console.log('๋ฆฌ๋ทฐ ์ˆ˜์ • ์‹คํŒจ', error);
},
});

const {
register,
handleSubmit,
formState: { errors },
clearErrors,
reset,
watch,
setValue,
setError,
} = useForm<ReviewForm>({
defaultValues: {
rating: reviewData.rating,
sliderLightBold: reviewData.sliderLightBold,
sliderSmoothTanic: reviewData.sliderSmoothTanic,
sliderdrySweet: reviewData.sliderdrySweet,
slidersoftAcidic: reviewData.slidersoftAcidic,
content: reviewData.content,
aroma: reviewData.aroma.map((eng) => {
const kor = Object.keys(aromaMap).find((key) => aromaMap[key] === eng);
return kor || eng;
}),
},
});

const aroma = watch('aroma');
const isSelected = (item: string) => aroma?.includes(item);

const toggleAroma = (item: string) => {
if (!aroma) return;
if (aroma.includes(item)) {
setValue(
'aroma',
aroma.filter((a) => a !== item),
);
} else {
setValue('aroma', [...aroma, item]);
}
clearErrors('aroma');
};

const onSubmit = async (data: ReviewForm) => {
if (!data.aroma || data.aroma.length === 0) {
setError('aroma', { type: 'errmsg', message: '์ตœ์†Œ ํ•˜๋‚˜์˜ ํ–ฅ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.' });
return;
}
const fullData = {
reviewId: reviewData.reviewId,
rating: data.rating,
lightBold: data.sliderLightBold,
smoothTannic: data.sliderSmoothTanic,
drySweet: data.sliderdrySweet,
softAcidic: data.slidersoftAcidic,
aroma: data.aroma.map((a) => aromaMap[a]).filter(Boolean),
content: data.content,
};
updateReviewMutation.mutate(fullData);
};

const closeModalReset = (isOpen: boolean) => {
setShowEditModal(isOpen);
if (!isOpen) {
reset();
}
};

return (
<div>
<Button variant='purpleDark' size='xs' width='sm' onClick={() => setShowEditModal(true)}>
๋ฆฌ๋ทฐ ์ˆ˜์ •ํ•˜๊ธฐ
</Button>
<BasicModal
type='review'
title='๋ฆฌ๋ทฐ ์ˆ˜์ •'
open={showEditModal}
onOpenChange={closeModalReset}
buttons={
<Button
onClick={handleSubmit(onSubmit)}
type='button'
variant='purpleDark'
size='xl'
width='full'
fontSize='lg'
>
์ˆ˜์ • ์™„๋ฃŒ
</Button>
}
>
<form
onSubmit={handleSubmit(onSubmit)}
encType='multipart/form-data'
className='my-[32px] md:my-[40px]'
>
<div className='w-[274px] md:w-[384px] h-[84px] md:h-[68px] mb-6 flex items-center'>
<Image
src='/assets/reviewicon.png'
alt='๋ฆฌ๋ทฐ ์•„์ด์ฝ˜'
width={68}
height={68}
className='bg-gray-100 rounded-lg p-[7px] object-contain'
/>
<div className='ml-4'>
<span className='custom-text-lg-bold md:custom-text-2lg-semibold'>{wineName}</span>
<div className='mt-2'>
<StarRating value={watch('rating')} onChange={(val) => setValue('rating', val)} />
</div>
</div>
</div>

<textarea
id='content'
{...register('content', {
required: '๋ฆฌ๋ทฐ ๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.',
onChange: () => clearErrors('content'),
})}
placeholder='ํ›„๊ธฐ๋ฅผ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”'
className={cn(
'h-[100px] md:h-[120px] w-full px-[20px] py-[14px] rounded-[16px] bg-white border border-gray-300 outline-none font-sans resize-none',
errors.content && 'border-red-500',
)}
rows={5}
/>
{errors.content && (
<div role='alert' className='text-red-500 mt-1'>
{errors.content.message}
</div>
)}

<p className='custom-text-2lg-bold md:custom-text-xl-bold mb-[24px] mt-[35px]'>
์™€์ธ์˜ ๋ง›์€ ์–ด๋• ๋‚˜์š”?
</p>

<div className='mb-[40px] space-y-[18px]'>
<FlavorSlider
value={watch('sliderLightBold')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderLightBold', val)}
labelLeft='๊ฐ€๋ฒผ์›Œ์š”'
labelRight='์ง„ํ•ด์š”'
badgeLabel='๋ฐ”๋””๊ฐ'
/>
<FlavorSlider
value={watch('sliderSmoothTanic')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderSmoothTanic', val)}
labelLeft='๋ถ€๋“œ๋Ÿฌ์›Œ์š”'
labelRight='๋–ซ์–ด์š”'
badgeLabel='ํƒ€๋‹Œ'
/>
<FlavorSlider
value={watch('sliderdrySweet')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('sliderdrySweet', val)}
labelLeft='๋“œ๋ผ์ดํ•ด์š”'
labelRight='๋‹ฌ์•„์š”'
badgeLabel='๋‹น๋„'
/>
<FlavorSlider
value={watch('slidersoftAcidic')}
min={0}
max={10}
step={1}
onChange={(val) => setValue('slidersoftAcidic', val)}
labelLeft='์•ˆ ์…”์š”'
labelRight='๋งŽ์ด ์…”์š”'
badgeLabel='์‚ฐ๋ฏธ'
/>
</div>

<p className='custom-text-2lg-bold md:custom-text-xl-bold'>๊ธฐ์–ต์— ๋‚จ๋Š” ํ–ฅ์ด ์žˆ๋‚˜์š”?</p>
{errors.aroma && (
<div role='alert' className='text-red-500 mt-1'>
{errors.aroma.message}
</div>
)}
<div className='relative flex flex-wrap gap-[10px] mt-6'>
{aromaOptions.map((item) => (
<Badge
key={item}
variant='chooseFlavor'
onClick={() => toggleAroma(item)}
className={cn(
'cursor-pointer px-2.5 md:px-[18px] py-1.5 md:py-2.5 hover:bg-primary-100 hover:text-primary hover:border-primary-100',
isSelected(item) && 'bg-primary text-white',
)}
>
{item}
</Badge>
))}
</div>
</form>
</BasicModal>
</div>
);
};

export default EditReviewModal;