diff --git a/src/features/animes/routes/Detail/Ratings/index.tsx b/src/features/animes/routes/Detail/Ratings/index.tsx index b8cd3340..383043f6 100644 --- a/src/features/animes/routes/Detail/Ratings/index.tsx +++ b/src/features/animes/routes/Detail/Ratings/index.tsx @@ -17,7 +17,7 @@ import { export default function Ratings({ starScoreAvg }: { starScoreAvg: number }) { return (
-

평점

+

별점

diff --git a/src/features/animes/routes/Detail/Reviews/index.tsx b/src/features/animes/routes/Detail/Reviews/index.tsx index 6cdfcd61..15a616c1 100644 --- a/src/features/animes/routes/Detail/Reviews/index.tsx +++ b/src/features/animes/routes/Detail/Reviews/index.tsx @@ -72,6 +72,11 @@ export default function Reviews({ isLike={review.isLike} likeCount={review.likeCount} createdAt={review.createdAt} + reviewId={review.reviewId} + animeId={review.animeId} + isSpoiler={review.isSpoiler} + content={review.content} + score={review.score} /> diff --git a/src/features/animes/routes/List/index.tsx b/src/features/animes/routes/List/index.tsx index 231e8ecf..daa7756c 100644 --- a/src/features/animes/routes/List/index.tsx +++ b/src/features/animes/routes/List/index.tsx @@ -34,7 +34,7 @@ const TabItems: TabItem[] = [ }, { id: "SCORE", - title: "평점순", + title: "별점순", }, ]; diff --git a/src/features/bookmarks/hooks/useToggleBookmark.ts b/src/features/bookmarks/hooks/useToggleBookmark.ts index 1a003e21..6e4652fc 100644 --- a/src/features/bookmarks/hooks/useToggleBookmark.ts +++ b/src/features/bookmarks/hooks/useToggleBookmark.ts @@ -17,6 +17,12 @@ export default function useToggleBookmark(animeId: number) { onSuccess: () => { queryClient.invalidateQueries(["profile", user?.name]); queryClient.invalidateQueries(["profile", user?.memberId, "bookmark"]); + queryClient.invalidateQueries([ + "profile", + user?.memberId, + "count", + "bookmark", + ]); queryClient.invalidateQueries(["bookmark", user?.memberId, animeId]); queryClient.invalidateQueries(["anime", animeId, user?.memberId]); }, diff --git a/src/features/common/routes/Home/RecentReview/index.tsx b/src/features/common/routes/Home/RecentReview/index.tsx index 11fe27b9..0cbe9318 100644 --- a/src/features/common/routes/Home/RecentReview/index.tsx +++ b/src/features/common/routes/Home/RecentReview/index.tsx @@ -49,6 +49,11 @@ export default function RecentReview() { isLike={data.pages[0].isLike} likeCount={data.pages[0].likeCount} isTimeAgo={true} + reviewId={data.pages[0].reviewId} + animeId={data.pages[0].anime.animeId} + isSpoiler={data.pages[0].isSpoiler} + content={data.pages[0].content} + score={data.pages[0].score} /> )} diff --git a/src/features/reviews/api/mock/recentReview1.json b/src/features/reviews/api/mock/recentReview1.json index 34958742..a225efbc 100644 --- a/src/features/reviews/api/mock/recentReview1.json +++ b/src/features/reviews/api/mock/recentReview1.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 10, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -15,7 +17,9 @@ } }, { + "reviewId": 9, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 8, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 7, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 6, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 5, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 4, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 3, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 2, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 1, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/reviews/api/mock/recentReview2.json b/src/features/reviews/api/mock/recentReview2.json index 8cdabf8d..3fe8c601 100644 --- a/src/features/reviews/api/mock/recentReview2.json +++ b/src/features/reviews/api/mock/recentReview2.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 20, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -15,7 +17,9 @@ } }, { + "reviewId": 19, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 18, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 17, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 16, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 15, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 14, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 13, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 12, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 11, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/reviews/api/mock/recentReview3.json b/src/features/reviews/api/mock/recentReview3.json index f177186e..763a1ec8 100644 --- a/src/features/reviews/api/mock/recentReview3.json +++ b/src/features/reviews/api/mock/recentReview3.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 30, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다. 리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다. 리뷰 입니다. 리뷰 입니다. 리뷰 입니다.", "isSpoiler": true, "isLike": false, @@ -15,7 +17,9 @@ } }, { + "reviewId": 29, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 28, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 27, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 26, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 25, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 24, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 23, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 22, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 21, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/reviews/api/mock/recentReviewOnlyOne.json b/src/features/reviews/api/mock/recentReviewOnlyOne.json index d125e52b..3f1cb1a3 100644 --- a/src/features/reviews/api/mock/recentReviewOnlyOne.json +++ b/src/features/reviews/api/mock/recentReviewOnlyOne.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 10, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": false, diff --git a/src/features/reviews/api/review.ts b/src/features/reviews/api/review.ts index 3cc62062..57650bf2 100644 --- a/src/features/reviews/api/review.ts +++ b/src/features/reviews/api/review.ts @@ -8,10 +8,8 @@ import recentReviewMock3 from "./mock/recentReview3.json"; import recentReviewOnlyOneMock from "./mock/recentReviewOnlyOne.json"; export type ReviewInfo = Omit & { - reviewId: number; animeId: number; thumbnail: string; - score: number; }; export type AddReviewDto = Pick & { @@ -30,6 +28,11 @@ export default class ReviewApi { return post("/short-reviews", review); } + /** @description 리뷰 수정 요청 */ + async updateReview(reviewId: number, review: AddReviewDto) { + return patch(`/short-reviews/${reviewId}`, review); + } + /** @description 한 애니의 리뷰 목록 요청 */ async getAnimeReviews( animeId: number, diff --git a/src/features/reviews/api/reviewDev.ts b/src/features/reviews/api/reviewDev.ts index 3766ea5b..eccb84a8 100644 --- a/src/features/reviews/api/reviewDev.ts +++ b/src/features/reviews/api/reviewDev.ts @@ -18,6 +18,11 @@ export default class ReviewDevApi { return post("/short-reviews", review); } + /** @description 리뷰 수정 요청 */ + async updateReview(reviewId: number, review: AddReviewDto) { + return patch(`/short-reviews/${reviewId}`, review); + } + /** @description 한 애니의 리뷰 목록 요청*/ async getAnimeReviews( animeId: number, diff --git a/src/features/reviews/components/ReviewCard/ActionBar.tsx b/src/features/reviews/components/ReviewCard/ActionBar.tsx index df5bdd90..6032bb4b 100644 --- a/src/features/reviews/components/ReviewCard/ActionBar.tsx +++ b/src/features/reviews/components/ReviewCard/ActionBar.tsx @@ -10,6 +10,11 @@ export interface ActionBarProps { likeCount: number; createdAt?: string; isTimeAgo?: boolean; + reviewId: number; + animeId: number; + content: string; + isSpoiler: boolean; + score: number; } export default function ActionBar({ isMine, @@ -17,6 +22,11 @@ export default function ActionBar({ likeCount, createdAt, isTimeAgo, + reviewId, + animeId, + content, + isSpoiler, + score, }: ActionBarProps) { const date = isTimeAgo ? timeAgo(createdAt) : dateWithDots(createdAt); @@ -29,7 +39,14 @@ export default function ActionBar({ count={compactNumber(likeCount, "ko-KR")} onClick={() => {}} /> - + ); diff --git a/src/features/reviews/components/ReviewCard/ReviewMoreButton.tsx b/src/features/reviews/components/ReviewCard/ReviewMoreButton.tsx index ceaa1b49..db6ca630 100644 --- a/src/features/reviews/components/ReviewCard/ReviewMoreButton.tsx +++ b/src/features/reviews/components/ReviewCard/ReviewMoreButton.tsx @@ -6,9 +6,11 @@ import { useState } from "react"; import BackdropPortal from "@/components/Backdrop/BackdropPortal"; import Rating from "@/components/Rating"; import useSnackBar from "@/components/SnackBar/useSnackBar"; +import useToast from "@/components/Toast/useToast"; import DropDownModal from "@/features/users/components/DropDownModal"; import useDropDownModal from "@/features/users/components/DropDownModal/useDropDownModal"; +import useEvaluation from "../../hook/useEvaluation"; import ShortReviewModal from "../ReviewRating/ShortReviewModal"; import { @@ -17,12 +19,8 @@ import { RatingContainer, } from "./ReviewMoreButton.style"; -const USER_MOCK_REVIEW_DATA = { - id: 1, - animeId: 1, - score: 7, - content: "유저가 생성한 짧은 리뷰입니다.", - isSpoiler: true, +// TODO: 서버에서 가져오기 +const USER_MOCK_ATTRACTION = { character: true, art: true, story: false, @@ -32,37 +30,61 @@ const USER_MOCK_REVIEW_DATA = { interface ReviewMoreButtonProps { isMine: boolean; + reviewId: number; + animeId: number; + content: string; + isSpoiler: boolean; + score: number; } -export default function ReviewMoreButton({ isMine }: ReviewMoreButtonProps) { +export default function ReviewMoreButton({ + isMine, + reviewId, + animeId, + content, + score, + isSpoiler, +}: ReviewMoreButtonProps) { const theme = useTheme(); const { isDropDownModalOpen, handleDropDownModalToggle } = useDropDownModal(); const snackBar = useSnackBar(); const [isReviewModalVisible, setIsReviewModalVisible] = useState(false); + + const evaluationMutation = useEvaluation({ animeId }); + + const toast = useToast(); + const handleReviewModalToggle = () => setIsReviewModalVisible((prev) => !prev); + const handleReviewEditClick = () => { handleDropDownModalToggle(); handleReviewModalToggle(); - // setIsReviewModalVisible(true); }; + const handleRate = (value: number) => { - // if (!user) { - // setIsLoginModalVisible(true); - // return; - // } - // TODO: 점수 등록 요청하기 + evaluationMutation.mutate( + { score: value }, + { + onSuccess: () => { + toast.success({ message: "별점이 수정되었어요." }); + }, + }, + ); console.log(value); }; + const handleReviewDeleteClick = () => console.log("리뷰삭제"); + const handleReviewSpoilerReport = () => { handleDropDownModalToggle(); - snackBar.open({ message: "신고가 접수되었습니다." }); + snackBar.open({ message: "신고가 접수되었어요." }); }; + const handleReviewEtcReport = () => { handleDropDownModalToggle(); snackBar.open({ - message: "신고가 접수되었습니다.", + message: "신고가 접수되었어요.", }); }; @@ -123,15 +145,17 @@ export default function ReviewMoreButton({ isMine }: ReviewMoreButtonProps) { onClose={() => setIsReviewModalVisible(false)} onReview={() => setIsReviewModalVisible(false)} showBackdrop={false} - userReviewData={USER_MOCK_REVIEW_DATA} + userReviewData={{ + reviewId, + animeId, + content, + isSpoiler, + ...USER_MOCK_ATTRACTION, + }} > 내 별점 - + )} diff --git a/src/features/reviews/components/ReviewRating/ShortReviewModal.tsx b/src/features/reviews/components/ReviewRating/ShortReviewModal.tsx index 596ebfbd..9a8843dc 100644 --- a/src/features/reviews/components/ReviewRating/ShortReviewModal.tsx +++ b/src/features/reviews/components/ReviewRating/ShortReviewModal.tsx @@ -16,7 +16,7 @@ import { import SpoilerCheckBox from "./SpoilerCheckBox"; export interface MOCK_USER_REVIEW_DATA { - id: number; + reviewId: number; animeId: number; content: string; isSpoiler: boolean; diff --git a/src/features/reviews/components/ReviewRating/index.tsx b/src/features/reviews/components/ReviewRating/index.tsx index f549a57b..ca3648c7 100644 --- a/src/features/reviews/components/ReviewRating/index.tsx +++ b/src/features/reviews/components/ReviewRating/index.tsx @@ -7,6 +7,7 @@ import useAuth from "@/features/auth/hooks/useAuth"; import useDebounce from "@/hooks/useDebounce"; import useEvaluation from "../../hook/useEvaluation"; +import useGetEvaluation from "../../hook/useGetEvaluation"; import ShortReviewModal from "./ShortReviewModal"; import { ReviewRecommend } from "./style"; @@ -18,13 +19,14 @@ interface ReviewRatingProps { const DEBOUNCE_DELAY = 200; export default function ReviewRating({ animeId }: ReviewRatingProps) { - const hasReviewed = true; // dev용 변수 + const hasReview = true; // dev용 변수 const { user } = useAuth(); const [isLoginModalVisible, setIsLoginModalVisible] = useState(false); const [isReviewModalVisible, setIsReviewModalVisible] = useState(false); - const { data: evaluation, evaluationMutation } = useEvaluation(animeId); + const { data: evaluation } = useGetEvaluation(animeId); + const evaluationMutation = useEvaluation({ animeId, hasReview }); const handleRate = useDebounce((value: number) => { if (!user) { @@ -32,7 +34,11 @@ export default function ReviewRating({ animeId }: ReviewRatingProps) { return; } // 평가 추가 또는 기존과 다른 점수로 평가 수정 시에만 요청 수행 - if (evaluation?.score !== value) evaluationMutation.mutate(value); + if (evaluation?.score !== value) + evaluationMutation.mutate({ + score: value, + hasPrevData: Boolean(evaluation), + }); }, DEBOUNCE_DELAY); return ( @@ -49,7 +55,7 @@ export default function ReviewRating({ animeId }: ReviewRatingProps) { )} - {hasReviewed && "TODO: 사용자 리뷰 렌더링 "} + {hasReview && "TODO: 사용자 리뷰 렌더링 "} {isReviewModalVisible && ( diff --git a/src/features/reviews/hook/useAddReview.ts b/src/features/reviews/hook/useAddReview.ts deleted file mode 100644 index 9bdd61f8..00000000 --- a/src/features/reviews/hook/useAddReview.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import useAuth from "@/features/auth/hooks/useAuth"; -import { useApi } from "@/hooks/useApi"; - -import { AddReviewDto } from "../api/review"; - -export default function useAddReview(animeId: number, onReview: () => void) { - const queryClient = useQueryClient(); - const { reviewApi } = useApi(); - const { user } = useAuth(); - - return useMutation({ - mutationFn: (review: AddReviewDto) => reviewApi.addReview(review), - onSuccess: () => { - queryClient.invalidateQueries(["profile", user?.name]); - queryClient.invalidateQueries(["profile", user?.memberId, "review"]); - queryClient.invalidateQueries(["review", animeId, user?.memberId]); - queryClient.invalidateQueries(["anime", animeId, user?.memberId]); - // TODO: 최신 리뷰 목록 query 무효화 - onReview(); - }, - }); -} diff --git a/src/features/reviews/hook/useEvaluation.ts b/src/features/reviews/hook/useEvaluation.ts index 492e917c..1d41b293 100644 --- a/src/features/reviews/hook/useEvaluation.ts +++ b/src/features/reviews/hook/useEvaluation.ts @@ -1,31 +1,32 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import useAuth from "@/features/auth/hooks/useAuth"; import { useApi } from "@/hooks/useApi"; import { useCommonToastError } from "@/libs/error"; -export default function useEvaluation(animeId: number) { +export default function useEvaluation({ + animeId, + hasReview = true, +}: { + animeId: number; + hasReview?: boolean; +}) { const queryClient = useQueryClient(); const { reviewApi } = useApi(); const { user } = useAuth(); const { toastAuthError, toastDefaultError } = useCommonToastError(); - const { data } = useQuery({ - queryKey: ["evaluation", animeId, user?.memberId], - queryFn: async () => { - try { - return await reviewApi.getEvaluation(animeId); - } catch (e) { - return null; - } - }, - }); - - const evaluationMutation = useMutation({ - mutationFn: (score: number) => { - if (!data) return reviewApi.addEvaluation(animeId, score); + return useMutation({ + mutationFn: ({ + score, + hasPrevData = true, + }: { + score: number; + hasPrevData?: boolean; + }) => { + if (!hasPrevData) return reviewApi.addEvaluation(animeId, score); return reviewApi.updateEvaluation(animeId, score); }, onSuccess: () => { @@ -35,6 +36,14 @@ export default function useEvaluation(animeId: number) { queryClient.invalidateQueries(["averageRating", animeId, user?.memberId]); // 프로필 북마크 query 무효화 queryClient.invalidateQueries(["profile", user?.memberId, "bookmark"]); + + // 별점 수정 대상 애니에 대한 사용자의 리뷰가 존재하는 경우에만 리뷰 목록 조회 무효화 + if (hasReview) { + queryClient.invalidateQueries(["profile", user?.memberId, "review"]); + queryClient.invalidateQueries(["review", animeId, user?.memberId]); + queryClient.invalidateQueries(["anime", animeId, user?.memberId]); + // TODO: 최신 리뷰 목록 조회 무효화 + } }, onError: (error) => { if (error instanceof AxiosError && error.response?.status) { @@ -50,6 +59,4 @@ export default function useEvaluation(animeId: number) { } }, }); - - return { data, evaluationMutation }; } diff --git a/src/features/reviews/hook/useGetAnimeReviews.ts b/src/features/reviews/hook/useGetAnimeReviews.ts index cec997ee..78dbcc95 100644 --- a/src/features/reviews/hook/useGetAnimeReviews.ts +++ b/src/features/reviews/hook/useGetAnimeReviews.ts @@ -32,12 +32,12 @@ export default function useGetAnimeReviews(animeId: number) { order: "DESC", }, { - label: "평점 높은 순", + label: "별점 높은 순", sort: "score", order: "DESC", }, { - label: "평점 낮은 순", + label: "별점 낮은 순", sort: "score", order: "ASC", }, diff --git a/src/features/reviews/hook/useGetEvaluation.ts b/src/features/reviews/hook/useGetEvaluation.ts new file mode 100644 index 00000000..c28a1d97 --- /dev/null +++ b/src/features/reviews/hook/useGetEvaluation.ts @@ -0,0 +1,20 @@ +import { useQuery } from "@tanstack/react-query"; + +import useAuth from "@/features/auth/hooks/useAuth"; +import { useApi } from "@/hooks/useApi"; + +export default function useGetEvaluation(animeId: number) { + const { reviewApi } = useApi(); + const { user } = useAuth(); + + return useQuery({ + queryKey: ["evaluation", animeId, user?.memberId], + queryFn: async () => { + try { + return await reviewApi.getEvaluation(animeId); + } catch (e) { + return null; + } + }, + }); +} diff --git a/src/features/reviews/hook/useReview.ts b/src/features/reviews/hook/useReview.ts new file mode 100644 index 00000000..f1151adf --- /dev/null +++ b/src/features/reviews/hook/useReview.ts @@ -0,0 +1,67 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import useAuth from "@/features/auth/hooks/useAuth"; +import { useApi } from "@/hooks/useApi"; +import { useCommonToastError } from "@/libs/error"; + +import { AddReviewDto } from "../api/review"; + +export default function useReview(animeId: number, onReview: () => void) { + const queryClient = useQueryClient(); + const { reviewApi } = useApi(); + const { user } = useAuth(); + + const { toastAuthError, toastDefaultError } = useCommonToastError(); + + const handleError = (error: unknown) => { + if (error instanceof AxiosError && error.response?.status) { + const status = error.response.status; + if ([401, 403].includes(status)) { + toastAuthError(); + } else if (status >= 500) { + toastDefaultError(); + } + } + }; + + // 리뷰 추가 + const addReview = useMutation({ + mutationFn: (review: AddReviewDto) => reviewApi.addReview(review), + onSuccess: () => { + queryClient.invalidateQueries(["profile", user?.name]); + queryClient.invalidateQueries(["profile", user?.memberId, "review"]); + queryClient.invalidateQueries([ + "profile", + user?.memberId, + "count", + "review", + ]); + queryClient.invalidateQueries(["review", animeId, user?.memberId]); + queryClient.invalidateQueries(["anime", animeId, user?.memberId]); + // TODO: 최신 리뷰 목록 query 무효화 + onReview(); + }, + onError: (error) => handleError(error), + }); + + // 리뷰 수정 + const updateReview = useMutation({ + mutationFn: ({ + reviewId, + review, + }: { + reviewId: number; + review: AddReviewDto; + }) => reviewApi.updateReview(reviewId, review), + onSuccess: () => { + queryClient.invalidateQueries(["profile", user?.memberId, "review"]); + queryClient.invalidateQueries(["review", animeId, user?.memberId]); + // TODO: 최신 리뷰 목록 query 무효화 + onReview(); + }, + onError: (error) => handleError(error), + }); + + return { addReview, updateReview }; +} diff --git a/src/features/reviews/hook/useReviewForm.tsx b/src/features/reviews/hook/useReviewForm.tsx index cb62c75b..cfa899a2 100644 --- a/src/features/reviews/hook/useReviewForm.tsx +++ b/src/features/reviews/hook/useReviewForm.tsx @@ -1,14 +1,12 @@ -import { AxiosError } from "axios"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import useToast from "@/components/Toast/useToast"; import useAuth from "@/features/auth/hooks/useAuth"; -import { useCommonToastError } from "@/libs/error"; import { MOCK_USER_REVIEW_DATA } from "../components/ReviewRating/ShortReviewModal"; -import useAddReview from "./useAddReview"; +import useReview from "./useReview"; export default function useReviewForm( onReview: () => void, @@ -19,10 +17,9 @@ export default function useReviewForm( const animeId = userReviewData?.animeId ?? Number(pathname.split("/")[2]); const { user } = useAuth(); - const reviewMutation = useAddReview(animeId, onReview); + const { addReview, updateReview } = useReview(animeId, onReview); const toast = useToast(); - const { toastAuthError, toastDefaultError } = useCommonToastError(); const [form, setForm] = useState({ content: userReviewData?.content ?? "", @@ -60,6 +57,7 @@ export default function useReviewForm( setError(true); return; } + // console.log(form); console.log({ name: user?.name, @@ -67,35 +65,54 @@ export default function useReviewForm( isSpoiler: form.isSpoiler, content: form.content, }); - // TODO: 새 리뷰 작성인지 수정인지 검사 - // 새 리뷰 작성 POST 요청 - reviewMutation.mutate( - { - name: user?.name ?? "", - animeId, - hasSpoiler: form.isSpoiler, - content: form.content, - }, - { - onSuccess: () => { - toast.success({ - message: "리뷰가 등록되었어요.", - buttonText: "내 모든 리뷰 보러 가기", - onClickButton: () => navigate("/profile"), - }); + + // 리뷰 수정 + if (userReviewData) { + // 내용에 변화가 있을 경우에만 요청 + if ( + userReviewData.content !== form.content || + userReviewData.isSpoiler !== form.isSpoiler + ) + updateReview.mutate( + { + reviewId: userReviewData.reviewId, + review: { + name: user?.name ?? "", + animeId, + hasSpoiler: form.isSpoiler, + content: form.content, + }, + }, + { + onSuccess: () => { + toast.success({ + message: "리뷰가 수정되었어요.", + }); + }, + }, + ); + // 모달 닫기 + else onReview(); + } else { + // 리뷰 추가 + addReview.mutate( + { + name: user?.name ?? "", + animeId, + hasSpoiler: form.isSpoiler, + content: form.content, }, - onError: (error) => { - if (error instanceof AxiosError && error.response?.status) { - const status = error.response.status; - if ([401, 403].includes(status)) { - toastAuthError(); - } else if (status >= 500) { - toastDefaultError(); - } - } + { + onSuccess: () => { + toast.success({ + message: "리뷰가 등록되었어요.", + buttonText: "내 모든 리뷰 보러 가기", + onClickButton: () => navigate("/profile"), + }); + }, }, - }, - ); + ); + } }; return { diff --git a/src/features/reviews/types/index.d.ts b/src/features/reviews/types/index.d.ts index 35a10d0e..7a72bab6 100644 --- a/src/features/reviews/types/index.d.ts +++ b/src/features/reviews/types/index.d.ts @@ -1,5 +1,7 @@ declare interface Review { + reviewId: number; name: string; + score: number; content: string; isSpoiler: boolean; isLike: boolean; diff --git a/src/features/users/api/mock/review1.json b/src/features/users/api/mock/review1.json index 34958742..a225efbc 100644 --- a/src/features/users/api/mock/review1.json +++ b/src/features/users/api/mock/review1.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 10, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -15,7 +17,9 @@ } }, { + "reviewId": 9, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 8, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 7, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 6, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 5, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 4, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 3, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 2, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 1, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/users/api/mock/review2.json b/src/features/users/api/mock/review2.json index 8cdabf8d..3fe8c601 100644 --- a/src/features/users/api/mock/review2.json +++ b/src/features/users/api/mock/review2.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 20, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -15,7 +17,9 @@ } }, { + "reviewId": 19, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 18, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 17, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 16, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 15, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 14, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 13, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 12, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 11, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/users/api/mock/review3.json b/src/features/users/api/mock/review3.json index f177186e..763a1ec8 100644 --- a/src/features/users/api/mock/review3.json +++ b/src/features/users/api/mock/review3.json @@ -1,7 +1,9 @@ { "items": [ { + "reviewId": 30, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다. 리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다.리뷰 입니다. 리뷰 입니다. 리뷰 입니다. 리뷰 입니다.", "isSpoiler": true, "isLike": false, @@ -15,7 +17,9 @@ } }, { + "reviewId": 29, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -29,7 +33,9 @@ } }, { + "reviewId": 28, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -43,7 +49,9 @@ } }, { + "reviewId": 27, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -57,7 +65,9 @@ } }, { + "reviewId": 26, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -71,7 +81,9 @@ } }, { + "reviewId": 25, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -85,7 +97,9 @@ } }, { + "reviewId": 24, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -99,7 +113,9 @@ } }, { + "reviewId": 23, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -113,7 +129,9 @@ } }, { + "reviewId": 22, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, @@ -127,7 +145,9 @@ } }, { + "reviewId": 21, "name": "faberjoo", + "score": 7, "content": "리뷰 입니다.", "isSpoiler": false, "isLike": true, diff --git a/src/features/users/routes/Profile/TabMenu/BookmarkDeleteModal.tsx b/src/features/users/routes/Profile/TabMenu/BookmarkDeleteModal.tsx index 6abf5d44..5158c884 100644 --- a/src/features/users/routes/Profile/TabMenu/BookmarkDeleteModal.tsx +++ b/src/features/users/routes/Profile/TabMenu/BookmarkDeleteModal.tsx @@ -41,6 +41,12 @@ export default function BookmarkDeleteModal({ onSuccess: () => { queryClient.invalidateQueries(["profile", user?.name]); queryClient.invalidateQueries(["profile", user?.memberId, "bookmark"]); + queryClient.invalidateQueries([ + "profile", + user?.memberId, + "count", + "bookmark", + ]); queryClient.invalidateQueries(["bookmark", user?.memberId, animeId]); queryClient.invalidateQueries(["anime", animeId, user?.memberId]); diff --git a/src/features/users/routes/Profile/TabMenu/ReviewList.tsx b/src/features/users/routes/Profile/TabMenu/ReviewList.tsx index c2b53c4d..942c2327 100644 --- a/src/features/users/routes/Profile/TabMenu/ReviewList.tsx +++ b/src/features/users/routes/Profile/TabMenu/ReviewList.tsx @@ -37,6 +37,11 @@ export default function ReviewList({ isMine, list }: ReviewListProps) { isMine={isMine} isLike={review.isLike} likeCount={review.likeCount} + reviewId={review.reviewId} + animeId={review.anime.animeId} + isSpoiler={review.isSpoiler} + content={review.content} + score={review.score} /> ))} diff --git a/src/features/users/routes/Profile/TabMenu/SortBar.tsx b/src/features/users/routes/Profile/TabMenu/SortBar.tsx index 681bc468..099ca89e 100644 --- a/src/features/users/routes/Profile/TabMenu/SortBar.tsx +++ b/src/features/users/routes/Profile/TabMenu/SortBar.tsx @@ -43,8 +43,8 @@ export default function SortBar({ queryKey: [ "profile", user?.memberId, - selectedMenu === "입덕애니" ? "bookmark" : "review", "count", + selectedMenu === "입덕애니" ? "bookmark" : "review", ], queryFn: () => profile.getTabListCount(user?.memberId, selectedMenu), });