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
5 changes: 3 additions & 2 deletions src/app/(with-header)/wines/[id]/_components/NoReview.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Image from 'next/image';
import emptyReview from '@/assets/icons/empty_review.svg';
import PostReviewModal from '@/components/modal/PostReviewModal';
import { AddReviewData } from './ReviewContainer';

export default function NoReview() {
export default function NoReview({ addReview }: { addReview: (newReview: AddReviewData) => void }) {
return (
<div>
<div className='my-[200px] flex flex-col items-center justify-center mobile:my-[100px]'>
<Image src={emptyReview} alt='리뷰 0개' width={136} height={136} />
<div className='mt-[24px] text-2lg text-gray-500'>작성된 리뷰가 없어요</div>
<div className='mt-[48px]'>
<PostReviewModal />
<PostReviewModal addReview={addReview} />
</div>
</div>
</div>
Expand Down
127 changes: 103 additions & 24 deletions src/app/(with-header)/wines/[id]/_components/ReviewContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { fetchWithAuth } from '@/lib/auth';
import { fetchWineDetail } from '@/lib/fetchWineDetail';
import { ReviewData } from '@/types/review-data';
import { calculateTasteAverage, getTopThreeAromas, calculateRatingCount } from '@/utils/ReviewUtils';
import ReviewTasteAverage from './ReviewTasteAverage';
Expand All @@ -10,61 +10,140 @@ import ReviewItem from './ReviewItem';
import ReviewRating from './ReviewRating';
import NoReview from './NoReview';

function ReviewList({ reviews }: { reviews: ReviewData['reviews'] }) {
export interface AddReviewData {
reviewId: number;
rating: number;
lightBold: number;
smoothTannic: number;
drySweet: number;
softAcidic: number;
aroma: string[];
content: string;
user: {
id: number;
nickname: string;
image: string;
};
wineId: number;
wineName: string;
}

export interface EditReviewData {
rating: number;
lightBold: number;
smoothTannic: number;
drySweet: number;
softAcidic: number;
aroma: string[];
content: string;
wineId: number;
}

function ReviewList({
reviews,
wineName,
deleteMyReview,
editMyReview,
}: {
reviews: ReviewData['reviews'];
wineName: string;
deleteMyReview: (id: number) => void;
editMyReview: (id: number, editReviewData: EditReviewData, updatedAt: string) => void;
}) {
return (
<div>
<div className='mb-[30px] text-xl font-bold'>리뷰 목록</div>
{reviews.map((review) => (
<ReviewItem key={review.id} review={review} />
<ReviewItem key={review.id} review={review} wineName={wineName} reviewInitialData={review} editMyReview={editMyReview} deleteMyReview={deleteMyReview} />
))}
</div>
);
}

export default function ReviewContainer() {
const { id } = useParams();
const wineId = typeof id === 'string' ? Number(id) : NaN;
const [reviews, setReviews] = useState<ReviewData['reviews']>([]);
const [wineName, setWineName] = useState<string>('');
const [avgRating, setAvgRating] = useState<number>(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [myReviewData, setMyReviewData] = useState<ReviewData['reviews']>([]);

useEffect(() => {
const fetchReviews = async () => {
try {
const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/wines/${id}`, {
method: 'GET',
});

if (!response) {
setError(' ');
setLoading(false);
return;
}
if (isNaN(wineId)) {
setError('유효한 와인 ID가 아닙니다.');
setLoading(false);
return;
}

const data: ReviewData = await response.json();
setLoading(true);
try {
const data = await fetchWineDetail(wineId);
setWineName(data.name);
setReviews(data.reviews);
setAvgRating(data.avgRating);
} catch (error: unknown) {
if (error instanceof Error) setError(`Error fetching reviews: ${error.message}`);
setMyReviewData(data.reviews);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
}
} finally {
setLoading(false);
}
};

if (id) {
if (wineId) {
fetchReviews();
} else {
setLoading(false);
}
}, [id]);
}, [wineId]);

const deleteMyReview = (id: number) => {
const updatedReviewList = myReviewData.filter((value) => value.id !== id);
setMyReviewData(updatedReviewList);

const updatedReviews = reviews.filter((value) => value.id !== id);
setReviews(updatedReviews);
};

const editMyReview = (id: number, editReviewData: EditReviewData, updatedAt: string) => {
const updatedReviewList = myReviewData.map((value) => {
if (value.id === id) {
return { ...value, ...editReviewData, updatedAt: updatedAt };
}
return value;
});
setMyReviewData(updatedReviewList);

const updatedReviews = reviews.map((value) => {
if (value.id === id) {
return { ...value, ...editReviewData, updatedAt: updatedAt };
}
return value;
});
setReviews(updatedReviews);
};

const addReview = (newReview: AddReviewData) => {
const formattedReview = {
...newReview,
id: newReview.reviewId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
wine: { id: wineId, name: wineName, image: '', avgRating: 0 },
isLiked: false,
};

setReviews((prevReviews) => [formattedReview, ...prevReviews]);
};

if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;

const averages = calculateTasteAverage(reviews);
const topThreeAromas = getTopThreeAromas(reviews);
const ratingPercentages = calculateRatingCount(reviews);

return (
<div>
{reviews.length > 0 ? (
Expand All @@ -84,11 +163,11 @@ export default function ReviewContainer() {

<div className='mt-[60px] flex justify-between gap-[60px] tablet:flex-col-reverse tablet:px-6'>
<div>
<ReviewList reviews={reviews} />
<ReviewList reviews={reviews} wineName={wineName} deleteMyReview={deleteMyReview} editMyReview={editMyReview} />
</div>
<div className='relative'>
<div className='sticky top-28'>
<ReviewRating avgRating={avgRating} count={reviews.length} ratingPercentages={ratingPercentages} />
<ReviewRating avgRating={avgRating} count={reviews.length} ratingPercentages={ratingPercentages} addReview={addReview} />
</div>
</div>
</div>
Expand All @@ -97,7 +176,7 @@ export default function ReviewContainer() {
) : (
<div className='mx-auto mt-[60px] w-full pc:max-w-[1140px] tablet:max-w-[1000px] tablet:px-6 mobile:max-w-[700px]'>
<div className='mb-[30px] text-xl font-bold'>리뷰 목록</div>
<NoReview />
<NoReview addReview={addReview} />
</div>
)}
</div>
Expand Down
82 changes: 67 additions & 15 deletions src/app/(with-header)/wines/[id]/_components/ReviewDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,74 @@
'use client';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import Dropdown from '@/components/Dropdown';
import menu from '@/assets/icons/menu.svg';
import { fetchWithAuth } from '@/lib/auth';
import Modal from '@/components/modal/Modal';
import DeleteWineForm from '@/components/modal/DeleteWineModal';
import PatchReviewForm from '@/components/modal/PatchReviewForm';
import { useState } from 'react';
import { MyReview } from '@/types/review-data';
import { EditReviewData } from './ReviewContainer';

export default function ReviewDropdown({ id }: { id: number }) {
const router = useRouter();
export default function ReviewDropdown({
id,
wineName,
reviewInitialData,
editMyReview,
deleteMyReview,
}: {
id: number;
wineName: string;
reviewInitialData: MyReview;
editMyReview: (id: number, editReviewData: EditReviewData, updatedAt: string) => void;
deleteMyReview: (id: number) => void;
}) {
const [isEditModalOpen, setIsEditModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);

const handleDelete = async () => {
const openEditModal = () => {
setIsEditModalOpen(true);
};

const closeEditModal = () => {
setIsEditModalOpen(false);
};

const openDeleteModal = () => {
setIsDeleteModalOpen(true);
};

const closeDeleteModal = () => {
setIsDeleteModalOpen(false);
};

const options = [
{ value: openEditModal, label: '수정하기' },
{ value: openDeleteModal, label: '삭제하기' },
];

const handleDeleteWine = async () => {
try {
const response = await fetchWithAuth(`${process.env.NEXT_PUBLIC_BASE_URL}/reviews/${id}`, {
method: 'DELETE',
});
if (response && response.ok) {
window.location.reload();
} else {
alert('삭제에 실패했습니다.');

if (!response?.ok || response === null) {
throw new Error('리뷰 삭제에 실패했습니다');
}

const body = await response.json();
if (body) {
if (deleteMyReview) {
deleteMyReview(id);
}
closeDeleteModal();
}
} catch (error) {
console.error(error);
alert('삭제 중 오류가 발생했습니다.');
console.error('리뷰 삭제 에러:', error);
}
};

const options = [
{ value: () => router.push(`/reviews/${id}`), label: '수정하기' },
{ value: () => handleDelete(), label: '삭제하기' },
];

return (
<div className='ml-[24px]'>
<Dropdown
Expand All @@ -39,6 +79,18 @@ export default function ReviewDropdown({ id }: { id: number }) {
>
<Image width={38} height={38} src={menu} className='mobile:h-[32px] mobile:w-[32px]' alt='메뉴 아이콘' />
</Dropdown>
<Modal
isOpen={isEditModalOpen}
setIsOpen={setIsEditModalOpen}
className={`overflow-x-hidden rounded-2xl mobile:mb-0 mobile:h-[930px] mobile:rounded-b-none ${
isEditModalOpen ? 'mobile:translate-y-0 mobile:animate-slide-up' : 'mobile:animate-slide-down mobile:translate-y-full'
}`}
>
<PatchReviewForm name={wineName} id={id} onClose={closeEditModal} reviewInitialData={reviewInitialData} editMyReview={editMyReview} />
</Modal>
<Modal isOpen={isDeleteModalOpen} setIsOpen={setIsDeleteModalOpen} className='rounded-2xl mobile:mx-auto mobile:h-[172px] mobile:w-[353px]'>
<DeleteWineForm onClose={closeDeleteModal} onDelete={handleDeleteWine} />
</Modal>
</div>
);
}
Loading