+
리뷰 목록
{totalCount.toLocaleString()}개 diff --git a/src/app/wines/[id]/_components/wine-review/wine-review-section.tsx b/src/app/wines/[id]/_components/wine-review/wine-review-section.tsx index 09171a0d..eed9df4e 100644 --- a/src/app/wines/[id]/_components/wine-review/wine-review-section.tsx +++ b/src/app/wines/[id]/_components/wine-review/wine-review-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; import RatingDistribution from "@/components/rating/rating-distribution"; import ReviewListHeader from "./wine-review-list-header"; @@ -35,16 +35,31 @@ const ReviewSection = ({ wineId, currentUserId, }: ReviewSectionProps) => { - const [visibleCount, setVisibleCount] = useState(REVIEW_PIECES); + const [visibleCount, setVisibleCount] = useState(() => + Math.min(REVIEW_PIECES, reviews.length) + ); + const visibleReviews = reviews.slice(0, visibleCount); - const hasMore = visibleCount < reviews.length; + + const hasMore = + reviews.length > REVIEW_PIECES && visibleCount < reviews.length; + + const prevLengthRef = useRef(reviews.length); useEffect(() => { - setVisibleCount((prev) => { - const nextLen = reviews.length; - if (nextLen < prev) return nextLen; - return prev; - }); + const prevLength = prevLengthRef.current; + const scrollY = window.scrollY; + + if (reviews.length > prevLength) { + setVisibleCount(Math.min(REVIEW_PIECES, reviews.length)); + } else if (reviews.length < prevLength) { + setVisibleCount((prev) => Math.min(prev, reviews.length)); + requestAnimationFrame(() => { + window.scrollTo(0, scrollY); + }); + } + + prevLengthRef.current = reviews.length; }, [reviews.length]); const handleLoadMore = () => { diff --git a/src/app/wines/[id]/_components/wine-taste/wine-taste-section.tsx b/src/app/wines/[id]/_components/wine-taste/wine-taste-section.tsx index 0552acf0..491a72ea 100644 --- a/src/app/wines/[id]/_components/wine-taste/wine-taste-section.tsx +++ b/src/app/wines/[id]/_components/wine-taste/wine-taste-section.tsx @@ -54,7 +54,7 @@ const WineTasteSection = ({ reviews, reviewCount }: WineTasteSectionProps) => { {/* 제목 + 갯수 */} {
{/* 와인 맛 분포 컴포넌트 */}
diff --git a/src/app/wines/[id]/_hooks/use-review-mutation.ts b/src/app/wines/[id]/_hooks/use-review-mutation.ts
index 46fe243f..e956bfdc 100644
--- a/src/app/wines/[id]/_hooks/use-review-mutation.ts
+++ b/src/app/wines/[id]/_hooks/use-review-mutation.ts
@@ -3,11 +3,11 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import postReview from "@/api/reviews/post-review";
import patchReview from "@/api/reviews/patch-review";
-import type { ReviewBase } from "@/types/wine";
+import type { ReviewBase, Review } from "@/types/wine";
interface ReviewMutationOptions {
mode: "create" | "edit";
- reviewId?: number; // edit일 때만 필요
+ reviewId?: number;
}
const useReviewMutation = ({ mode, reviewId }: ReviewMutationOptions) => {
@@ -21,23 +21,35 @@ const useReviewMutation = ({ mode, reviewId }: ReviewMutationOptions) => {
}
return postReview(data);
},
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["reviews", variables.wineId],
- });
- queryClient.invalidateQueries({
- queryKey: ["user-review"],
+ onSuccess: (response, variables) => {
+ const { wineId } = variables;
+
+ queryClient.setQueryData(["wine", wineId], (old: any) => {
+ if (!old) return old;
+
+ if (mode === "create") {
+ return {
+ ...old,
+ reviews: [response, ...old.reviews],
+ reviewCount: (old.reviewCount || 0) + 1,
+ };
+ } else if (mode === "edit") {
+ return {
+ ...old,
+ reviews: old.reviews.map((r: Review) =>
+ r.id === reviewId ? response : r
+ ),
+ };
+ }
+ return old;
});
- queryClient.invalidateQueries({ queryKey: ["wine", variables.wineId] });
+
if (mode === "edit" && reviewId) {
- queryClient.invalidateQueries({ queryKey: ["review", reviewId] });
+ queryClient.setQueryData(["review", reviewId], response);
}
- },
- onError: (error: unknown) => {
- console.error(
- mode === "edit" ? "리뷰 수정 실패:" : "리뷰 등록 실패:",
- error
- );
+
+ queryClient.invalidateQueries({ queryKey: ["wine", wineId] });
+ queryClient.invalidateQueries({ queryKey: ["user-review"] });
},
});
};
diff --git a/src/components/flavor/flavor.tsx b/src/components/flavor/flavor.tsx
index 71d14145..3981f708 100644
--- a/src/components/flavor/flavor.tsx
+++ b/src/components/flavor/flavor.tsx
@@ -1,19 +1,22 @@
import { AromaKey } from "@/types/aroma-type";
import Image from "next/image";
import { aromaMap } from "./aroma-map";
+import { cn } from "@/lib/utils";
+
interface FlavorItemProps {
aroma: AromaKey;
+ className?: string;
}
-const FlavorItem = ({ aroma }: FlavorItemProps) => {
+const FlavorItem = ({ aroma, className }: FlavorItemProps) => {
const { img, label } = aromaMap[aroma];
return (
-
+
{label}
@@ -24,22 +27,29 @@ const FlavorItem = ({ aroma }: FlavorItemProps) => {
interface FlavorProps {
count: number;
items: AromaKey[];
+ showHeader?: boolean;
}
-const Flavor = ({ count, items }: FlavorProps) => {
+const Flavor = ({ count, items, showHeader = true }: FlavorProps) => {
return (
-
-
-
+ - 어떤 향이 있나요? -
- - ({count}명 참여) - -
+ {showHeader && (
+
+
+ )}
+ 어떤 향이 있나요? +
+ + ({count}명 참여) + +
-
+
diff --git a/src/components/modal/page-modal.tsx b/src/components/modal/page-modal.tsx
index 70b8c3df..73b5b562 100644
--- a/src/components/modal/page-modal.tsx
+++ b/src/components/modal/page-modal.tsx
@@ -37,6 +37,9 @@ const PageModal = ({
return () => {
allowScroll(currentScrollY);
+ requestAnimationFrame(() => {
+ window.scrollTo(0, currentScrollY);
+ });
};
}, []);
diff --git a/src/hooks/api/reviews/use-review-delete.ts b/src/hooks/api/reviews/use-review-delete.ts
index 03ff53b3..c19c2c34 100644
--- a/src/hooks/api/reviews/use-review-delete.ts
+++ b/src/hooks/api/reviews/use-review-delete.ts
@@ -14,12 +14,17 @@ const useReviewDelete = ({ wineId, reviewId }: UseReviewDeleteOptions) => {
return useMutation({
mutationFn: () => deleteReview({ id: reviewId }),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["reviews", wineId] });
- queryClient.invalidateQueries({ queryKey: ["wine", wineId] });
+ queryClient.setQueryData(["wine", wineId], (old: any) => {
+ if (!old) return old;
+ return {
+ ...old,
+ reviews: old.reviews.filter((r: any) => r.id !== reviewId),
+ reviewCount: Math.max((old.reviewCount || 1) - 1, 0),
+ };
+ });
+
queryClient.invalidateQueries({ queryKey: ["review", reviewId] });
- },
- onError: (error: unknown) => {
- console.error("리뷰 삭제 실패:", error);
+ queryClient.invalidateQueries({ queryKey: ["user-review"] });
},
});
};
diff --git a/src/hooks/api/reviews/use-review-like.ts b/src/hooks/api/reviews/use-review-like.ts
index 05ad4df6..51484844 100644
--- a/src/hooks/api/reviews/use-review-like.ts
+++ b/src/hooks/api/reviews/use-review-like.ts
@@ -1,18 +1,31 @@
-"use client";
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
import postLike from "@/api/reviews/post-like";
import deleteLike from "@/api/reviews/delete-like";
+import type { Review } from "@/types/wine";
-const useReviewLike = (reviewId: number, wineId: number, isLiked: boolean) => {
+const useReviewLike = (reviewId: number, wineId: number) => {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: () => (isLiked ? deleteLike(reviewId) : postLike(reviewId)),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["reviews", wineId] });
- queryClient.invalidateQueries({ queryKey: ["wine", wineId] });
- queryClient.invalidateQueries({ queryKey: ["review", reviewId] });
+ mutationFn: (nextLike: boolean) =>
+ nextLike ? postLike(reviewId) : deleteLike(reviewId),
+ onMutate: async (nextLike) => {
+ await queryClient.cancelQueries({ queryKey: ["wine", wineId] });
+ const previous = queryClient.getQueryData(["wine", wineId]);
+
+ queryClient.setQueryData(["wine", wineId], (old: any) => ({
+ ...old,
+ reviews: old?.reviews.map((item: Review) =>
+ item.id === reviewId ? { ...item, isLiked: nextLike } : item
+ ),
+ }));
+
+ return { previous };
+ },
+ onError: (_err, _nextLike, context) => {
+ if (context?.previous) {
+ queryClient.setQueryData(["wine", wineId], context.previous);
+ }
},
});
};
{items.map((item, index) => (
-
+
))}