diff --git a/src/app/(with-header)/wines/[id]/_components/NoReview.tsx b/src/app/(with-header)/wines/[id]/_components/NoReview.tsx index 156f199..4dadd36 100644 --- a/src/app/(with-header)/wines/[id]/_components/NoReview.tsx +++ b/src/app/(with-header)/wines/[id]/_components/NoReview.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import emptyReview from '@/assets/icons/empty_review.svg'; import PostReviewModal from '@/components/modal/PostReviewModal'; -import { AddReviewData } from './ReviewContainer'; +import { AddReviewData } from '@/types/review-data'; export default function NoReview({ addReview }: { addReview: (newReview: AddReviewData) => void }) { return ( diff --git a/src/app/(with-header)/wines/[id]/_components/ReviewContainer.tsx b/src/app/(with-header)/wines/[id]/_components/ReviewContainer.tsx index cca64f8..df6591e 100644 --- a/src/app/(with-header)/wines/[id]/_components/ReviewContainer.tsx +++ b/src/app/(with-header)/wines/[id]/_components/ReviewContainer.tsx @@ -1,44 +1,15 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import { fetchWineDetail } from '@/lib/fetchWineDetail'; -import { ReviewData } from '@/types/review-data'; +import { useState } from 'react'; import { calculateTasteAverage, getTopThreeAromas, calculateRatingCount } from '@/utils/ReviewUtils'; +import { ReviewData } from '@/types/review-data'; +import { EditReviewData } from '@/types/review-data'; +import { AddReviewData } from '@/types/review-data'; import ReviewTasteAverage from './ReviewTasteAverage'; import ReviewAroma from './ReviewAroma'; import ReviewItem from './ReviewItem'; import ReviewRating from './ReviewRating'; import NoReview from './NoReview'; -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, @@ -60,69 +31,16 @@ function ReviewList({ ); } -export default function ReviewContainer() { - const { id } = useParams(); - const wineId = typeof id === 'string' ? Number(id) : NaN; - const [reviews, setReviews] = useState([]); - const [wineName, setWineName] = useState(''); - const [avgRating, setAvgRating] = useState(0); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [myReviewData, setMyReviewData] = useState([]); - - useEffect(() => { - const fetchReviews = async () => { - if (isNaN(wineId)) { - setError('유효한 와인 ID가 아닙니다.'); - setLoading(false); - return; - } - - setLoading(true); - try { - const data = await fetchWineDetail(wineId); - setWineName(data.name); - setReviews(data.reviews); - setAvgRating(data.avgRating); - setMyReviewData(data.reviews); - } catch (err) { - if (err instanceof Error) { - setError(err.message); - } - } finally { - setLoading(false); - } - }; - - if (wineId) { - fetchReviews(); - } - }, [wineId]); +export default function ReviewContainer({ data }: { data: ReviewData }) { + const { reviews, avgRating } = data; + const [localReviews, setLocalReviews] = useState(reviews); const deleteMyReview = (id: number) => { - const updatedReviewList = myReviewData.filter((value) => value.id !== id); - setMyReviewData(updatedReviewList); - - const updatedReviews = reviews.filter((value) => value.id !== id); - setReviews(updatedReviews); + setLocalReviews((prevReviews) => prevReviews.filter((review) => review.id !== id)); }; 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); + setLocalReviews((prevReviews) => prevReviews.map((review) => (review.id === id ? { ...review, ...editReviewData, updatedAt } : review))); }; const addReview = (newReview: AddReviewData) => { @@ -131,16 +49,13 @@ export default function ReviewContainer() { id: newReview.reviewId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - wine: { id: wineId, name: wineName, image: '', avgRating: 0 }, + wine: { id: data.id, name: data.name, image: '', avgRating: 0 }, isLiked: false, }; - setReviews((prevReviews) => [formattedReview, ...prevReviews]); + setLocalReviews((prevReviews) => [formattedReview, ...prevReviews]); }; - if (loading) return
Loading...
; - if (error) return
{error}
; - const averages = calculateTasteAverage(reviews); const topThreeAromas = getTopThreeAromas(reviews); const ratingPercentages = calculateRatingCount(reviews); @@ -163,7 +78,7 @@ export default function ReviewContainer() {
- +
diff --git a/src/app/(with-header)/wines/[id]/_components/ReviewDropdown.tsx b/src/app/(with-header)/wines/[id]/_components/ReviewDropdown.tsx index bdae9a6..caf9c58 100644 --- a/src/app/(with-header)/wines/[id]/_components/ReviewDropdown.tsx +++ b/src/app/(with-header)/wines/[id]/_components/ReviewDropdown.tsx @@ -8,7 +8,7 @@ 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'; +import { EditReviewData } from '@/types/review-data'; export default function ReviewDropdown({ id, diff --git a/src/app/(with-header)/wines/[id]/_components/ReviewItem.tsx b/src/app/(with-header)/wines/[id]/_components/ReviewItem.tsx index 64ab800..825d145 100644 --- a/src/app/(with-header)/wines/[id]/_components/ReviewItem.tsx +++ b/src/app/(with-header)/wines/[id]/_components/ReviewItem.tsx @@ -13,7 +13,7 @@ import elapsedTime from '@/utils/formatDate'; import ProfileImg from '@/components/ProfileImg'; import ReviewTasteItem from './ReviewTasteItem'; import ReviewDropdown from './ReviewDropdown'; -import { EditReviewData } from './ReviewContainer'; +import { EditReviewData } from '@/types/review-data'; type ReviewItemProps = { review: ReviewData['reviews'][0]; diff --git a/src/app/(with-header)/wines/[id]/_components/ReviewRating.tsx b/src/app/(with-header)/wines/[id]/_components/ReviewRating.tsx index 1772f10..4295bbe 100644 --- a/src/app/(with-header)/wines/[id]/_components/ReviewRating.tsx +++ b/src/app/(with-header)/wines/[id]/_components/ReviewRating.tsx @@ -1,7 +1,7 @@ 'use client'; import StaticRating from '@/components/StaticRating'; import PostReviewModal from '@/components/modal/PostReviewModal'; -import { AddReviewData } from './ReviewContainer'; +import { AddReviewData } from '@/types/review-data'; type ReviewRatingProps = { count: number; diff --git a/src/app/(with-header)/wines/[id]/_components/WineContainer.tsx b/src/app/(with-header)/wines/[id]/_components/WineContainer.tsx index 6910694..ee809f5 100644 --- a/src/app/(with-header)/wines/[id]/_components/WineContainer.tsx +++ b/src/app/(with-header)/wines/[id]/_components/WineContainer.tsx @@ -1,45 +1,8 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { useParams } from 'next/navigation'; -import { fetchWineDetail } from '@/lib/fetchWineDetail'; import { ReviewData } from '@/types/review-data'; import WineCard from '@/components/WineCard'; -export default function WineContainer() { - const { id } = useParams(); - const [wine, setWine] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchWineData = async () => { - if (id) { - const wineId = typeof id === 'string' ? Number(id) : NaN; - if (isNaN(wineId)) { - setError('유효한 와인 ID가 아닙니다.'); - setLoading(false); - return; - } - - try { - const wineData = await fetchWineDetail(wineId); - setWine(wineData); - } catch (error: unknown) { - if (error instanceof Error) setError(error.message); - } finally { - setLoading(false); - } - } else { - setLoading(false); - } - }; - - fetchWineData(); - }, [id]); - - if (loading) return
Loading...
; - if (error) return
{error}
; - +export default function WineContainer({ wine }: { wine: ReviewData }) { return (
{wine ? : ''}
diff --git a/src/app/(with-header)/wines/[id]/_components/skeleton/WineContainerSkeleton.tsx b/src/app/(with-header)/wines/[id]/_components/skeleton/WineContainerSkeleton.tsx new file mode 100644 index 0000000..e2b4149 --- /dev/null +++ b/src/app/(with-header)/wines/[id]/_components/skeleton/WineContainerSkeleton.tsx @@ -0,0 +1,14 @@ +export default function WineContainerSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/app/(with-header)/wines/[id]/page.tsx b/src/app/(with-header)/wines/[id]/page.tsx index 69a957e..ffef1c4 100644 --- a/src/app/(with-header)/wines/[id]/page.tsx +++ b/src/app/(with-header)/wines/[id]/page.tsx @@ -1,11 +1,85 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { fetchWineDetail } from '@/lib/fetchWineDetail'; +import { ReviewData } from '@/types/review-data'; import ReviewContainer from './_components/ReviewContainer'; import WineContainer from './_components/WineContainer'; +import WineContainerSkeleton from './_components/skeleton/WineContainerSkeleton'; +import NotFound from '@/app/not-found'; +import Refresh from '@/components/Refresh'; +import { useRouter } from 'next/navigation'; +import { useCallback } from 'react'; export default function Page() { + const { id } = useParams(); + const wineId = typeof id === 'string' ? Number(id) : NaN; + const [data, setData] = useState(null); + const [notFound, setNotFound] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const router = useRouter(); + + const fetchWineData = useCallback( + async (id: number) => { + setLoading(true); + setError(false); + setNotFound(false); + + try { + const data = await fetchWineDetail(id); + setData(data); + } catch (err) { + if (err instanceof Error) { + if (err.message === 'NOT_FOUND') { + setNotFound(true); + } else if (err.message === 'UNAUTHORIZED') { + alert('로그인 후, 이용해 주세요.'); + router.push('/signin'); + } else { + setError(true); + } + } else { + setError(true); + } + } finally { + setLoading(false); + } + }, + [router], + ); + + useEffect(() => { + if (isNaN(wineId)) { + setNotFound(true); + setLoading(false); + } else { + fetchWineData(wineId); + } + }, [wineId, fetchWineData]); + + if (loading) return ; + if (notFound || isNaN(wineId)) return ; + if (error) { + return ( +
+
+ fetchWineDetail(wineId)} + boxStyle='mx-auto h-[228px] rounded-[16px] tablet:w-full mobile:w-full gap-[10px]' + buttonStyle='px-[20px] py-[8px]' + iconSize='w-[100px] h-[100px] mobile:w-[60px] mobile:h-[60px]' + iconTextGap='gap-[10px]' + /> +
+
+ ); + } + return ( <> - - + {data && } + {data && } ); } diff --git a/src/components/modal/PostReviewModal.tsx b/src/components/modal/PostReviewModal.tsx index 7e3fd3d..6b7f9ec 100644 --- a/src/components/modal/PostReviewModal.tsx +++ b/src/components/modal/PostReviewModal.tsx @@ -11,7 +11,7 @@ import close from '@/assets/icons/close.svg'; import wineIcon from '@/assets/icons/wine.svg'; import InteractiveRating from '../InteractiveRating'; import ControlBar from '../ControlBar'; -import { AddReviewData } from '@/app/(with-header)/wines/[id]/_components/ReviewContainer'; +import { AddReviewData } from '@/types/review-data'; interface FormValues { rating: number; diff --git a/src/lib/fetchWineDetail.ts b/src/lib/fetchWineDetail.ts index 75a866c..8dbecd5 100644 --- a/src/lib/fetchWineDetail.ts +++ b/src/lib/fetchWineDetail.ts @@ -8,19 +8,21 @@ export const fetchWineDetail = async (id: number): Promise => { }); if (!response) { - throw new Error('로그인 후, 이용해 주세요.'); + throw new Error('UNAUTHORIZED'); } if (!response.ok) { - throw new Error('와인 상세 정보를 불러오는 데 실패했습니다.'); + if (response.status === 404) { + throw new Error('NOT_FOUND'); + } + throw new Error('FETCH_ERROR'); } - const data: ReviewData = await response.json(); - return data; + return await response.json(); } catch (error: unknown) { if (error instanceof Error) { - throw new Error(`와인 데이터 로드 실패: ${error.message}`); + throw new Error(error.message); } - throw new Error('알 수 없는 오류가 발생했습니다.'); + throw new Error('UNKNOWON_ERROR'); } }; diff --git a/src/types/review-data.ts b/src/types/review-data.ts index 35ea67d..b21d84b 100644 --- a/src/types/review-data.ts +++ b/src/types/review-data.ts @@ -52,3 +52,32 @@ export interface MyReview { }; wine: Wine; } + +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; +}