Skip to content

Commit 0bd2e42

Browse files
authored
Merge pull request #86 from SOPT-all/feat/product-like/#85
[feat/#85] 작품 좋아요 낙관적 업데이트 적용
2 parents 2b03358 + 3e86ce6 commit 0bd2e42

File tree

3 files changed

+75
-53
lines changed

3 files changed

+75
-53
lines changed

src/apis/mutations/use-product-like.mutation.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import { API_ENDPOINTS } from "@/apis/constants/api-endpoints";
44
import { HTTPMethod, request } from "@/apis/request";
55
import type {
6+
ProductInfoResponse,
67
ProductLikeRequest,
78
ProductLikeResponse,
89
} from "@/apis/types/product";
9-
import { useMutation } from "@tanstack/react-query";
10+
import { useMutation, useQueryClient } from "@tanstack/react-query";
11+
import { productQueryKeys } from "@/apis/constants/query-key";
1012

1113
export const postProductLike = ({ productId, userId }: ProductLikeRequest) => {
1214
return request<ProductLikeResponse>({
@@ -19,7 +21,52 @@ export const postProductLike = ({ productId, userId }: ProductLikeRequest) => {
1921
};
2022

2123
export const useProductLikeMutation = () => {
24+
const queryClient = useQueryClient();
25+
2226
return useMutation({
2327
mutationFn: postProductLike,
28+
onMutate: async ({ productId }) => {
29+
// 진행 중인 쿼리 취소
30+
await queryClient.cancelQueries({
31+
queryKey: productQueryKeys.detail(productId),
32+
});
33+
34+
// 이전 데이터 백업
35+
const prevData = queryClient.getQueryData<ProductInfoResponse>(
36+
productQueryKeys.detail(productId)
37+
);
38+
39+
// setQueryData로 캐시 직접 조작
40+
queryClient.setQueryData<ProductInfoResponse>(
41+
productQueryKeys.detail(productId), // 어떤 캐시에 접근할지 지정
42+
(
43+
old // 캐시된 데이터를 받아서
44+
) =>
45+
old // 데이터가 존재하면
46+
? {
47+
...old, // 나머지 데이터는 그대로 유지
48+
isLiked: !old.isLiked, // isLiked 필드를 현재와 반대로 토글하고
49+
likeCount: old.likeCount + (old.isLiked ? -1 : 1), // likeCount를 1+/-
50+
}
51+
: old // 캐시된 데이터가 없다면 그대로 둠
52+
);
53+
54+
return { prevData };
55+
},
56+
onError: (_, { productId }, context) => {
57+
// 실패 시 onMutate에서 백업해둔 이전 데이터로 캐시 원상복구
58+
if (context?.prevData) {
59+
queryClient.setQueryData(
60+
productQueryKeys.detail(productId),
61+
context.prevData
62+
);
63+
}
64+
},
65+
onSuccess: (_, { productId }) => {
66+
// 새로운 데이터로 갱신
67+
queryClient.invalidateQueries({
68+
queryKey: productQueryKeys.detail(productId),
69+
});
70+
},
2471
});
2572
};

src/shared/components/footer/footer.tsx

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,51 @@
1+
import { Suspense } from "react";
12
import { LikeButton } from "@/shared/components/like-button/like-button";
23
import * as styles from "./footer.css";
3-
import { useState } from "react";
44
import SmallButton from "@/shared/components/button/small-button/small-button";
55
import { SMALL_BUTTON_VARIANTS } from "@/shared/constants/button";
66
import BottomSheet from "@/shared/components/bottom-sheet/bottom-sheet";
77
import useBottomSheet from "@/shared/components/bottom-sheet/hooks/use-bottom-sheet";
88
import FooterDetail from "@/shared/components/footer-detail/footer-detail";
99
import Purchase from "@/pages/purchase/purchase";
1010
import { useProductLikeMutation } from "@/apis/mutations/use-product-like.mutation";
11+
import { useProductInfo } from "@/apis/queries/use-product-info.query";
1112

12-
const Footer = () => {
13-
const [isProductLiked, setIsProductLiked] = useState(false);
14-
const { isOpen, open, close } = useBottomSheet();
13+
const ProductLikeButton = () => {
14+
const productId = 1;
15+
const userId = 1;
16+
17+
const { data: productInfo } = useProductInfo({ productId, userId });
1518
const { mutate: likeProduct } = useProductLikeMutation();
16-
const [likeCount, setLikeCount] = useState(0);
1719

18-
// TODO: 페이지 내 훅으로 이동
1920
const handleProductLike = () => {
20-
likeProduct(
21-
{ productId: 1, userId: 1 },
22-
{
23-
onSuccess: (data) => {
24-
setIsProductLiked((prev) => !prev);
25-
setLikeCount(data.likeCount);
26-
},
27-
onError: (error) => {
28-
console.error("좋아요 실패:", error);
29-
},
30-
}
31-
);
21+
likeProduct({ productId, userId });
3222
};
3323

24+
return (
25+
<LikeButton
26+
variant="bottom-sheets"
27+
liked={productInfo.isLiked}
28+
count={productInfo.likeCount}
29+
onClick={handleProductLike}
30+
/>
31+
);
32+
};
33+
34+
const Footer = () => {
35+
const { isOpen, open, close } = useBottomSheet();
36+
3437
return (
3538
<footer className={styles.layout}>
3639
<div className={styles.wrapper}>
3740
<FooterDetail />
3841

3942
<div className={styles.container}>
40-
<LikeButton
41-
variant="bottom-sheets"
42-
liked={isProductLiked}
43-
count={likeCount}
44-
onClick={handleProductLike}
45-
/>
43+
<Suspense
44+
fallback={
45+
<LikeButton variant="bottom-sheets" liked={false} count={0} />
46+
}>
47+
<ProductLikeButton />
48+
</Suspense>
4649

4750
<div className={styles.buttonContainer}>
4851
<SmallButton

vite.config.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
55
import svgr from "vite-plugin-svgr";
66
import path from "node:path";
77
import { fileURLToPath } from "node:url";
8-
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
9-
import { playwright } from "@vitest/browser-playwright";
108

119
const dirname =
1210
typeof __dirname !== "undefined"
@@ -37,30 +35,4 @@ export default defineConfig({
3735
"@apis": path.resolve(dirname, "./src/apis"),
3836
},
3937
},
40-
test: {
41-
projects: [
42-
{
43-
extends: true,
44-
plugins: [
45-
storybookTest({
46-
configDir: path.join(dirname, ".storybook"),
47-
}),
48-
],
49-
test: {
50-
name: "storybook",
51-
browser: {
52-
enabled: true,
53-
headless: true,
54-
provider: playwright({}),
55-
instances: [
56-
{
57-
browser: "chromium",
58-
},
59-
],
60-
},
61-
setupFiles: [".storybook/vitest.setup.ts"],
62-
},
63-
},
64-
],
65-
},
6638
});

0 commit comments

Comments
 (0)