diff --git a/src/app/wines/[id]/_components/wine-review/wine-review-item.tsx b/src/app/wines/[id]/_components/wine-review/wine-review-item.tsx index 23529d04..a86a0eb8 100644 --- a/src/app/wines/[id]/_components/wine-review/wine-review-item.tsx +++ b/src/app/wines/[id]/_components/wine-review/wine-review-item.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef, useMemo, memo } from "react"; +import { useState, useRef, useMemo, memo, useEffect } from "react"; import { useRouter } from "next/navigation"; import { cn, getAromaIconName } from "@/lib/utils"; import WineTaste, { buildTasteData } from "@/components/wine-taste"; @@ -78,7 +78,7 @@ const WineReviewItem = ({ const router = useRouter(); const { reviewDeleteSuccess, reviewDeleteError } = useToast(); - const isLiked = useMemo(() => { + const derivedIsLiked = useMemo(() => { return typeof review.isLiked === "boolean" ? review.isLiked : Object.keys(review.isLiked).length > 0; @@ -86,6 +86,11 @@ const WineReviewItem = ({ const [isTasteOpen, setIsTasteOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [liked, setLiked] = useState(derivedIsLiked); + + useEffect(() => { + setLiked(derivedIsLiked); + }, [derivedIsLiked]); const { isOn: isMenuOpen, @@ -97,8 +102,7 @@ const WineReviewItem = ({ const { mutate: toggleLike, isPending: likePending } = useReviewLike( review.id, - wineId, - isLiked + wineId ); const { mutate: deleteReview, isPending: deletePending } = useReviewDelete({ @@ -136,7 +140,6 @@ const WineReviewItem = ({ deleteReview(undefined, { onSuccess: () => { setIsDeleteModalOpen(false); - requestAnimationFrame(() => window.scrollTo(0, scrollYBeforeDelete)); reviewDeleteSuccess(); }, onError: () => { @@ -145,6 +148,17 @@ const WineReviewItem = ({ }); }; + const handleLikeClick = () => { + const next = !liked; + setLiked(next); + + toggleLike(next, { + onError: () => { + setLiked((prev) => !prev); + }, + }); + }; + return ( <>
{!isMyReview && ( { - toggleLike(); - }} - aria-label={isLiked ? "좋아요 취소" : "좋아요"} + onClick={handleLikeClick} + aria-label={liked ? "좋아요 취소" : "좋아요"} /> )} {isMyReview && ( diff --git a/src/hooks/api/reviews/use-review-like.ts b/src/hooks/api/reviews/use-review-like.ts index 05ad4df6..6eaa8391 100644 --- a/src/hooks/api/reviews/use-review-like.ts +++ b/src/hooks/api/reviews/use-review-like.ts @@ -1,18 +1,37 @@ -"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: ["reviews", wineId] }); + const previous = queryClient.getQueryData(["reviews", wineId]); + + queryClient.setQueryData(["reviews", wineId], (old?: Review[]) => + old?.map((item) => + item.id === reviewId ? { ...item, isLiked: nextLike } : item + ) + ); + + 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(["reviews", wineId], context.previous); + } }, }); };