Skip to content

Commit d3138a9

Browse files
authored
feat: 절약 팁 조회와 북마크를 연동한다 (#249)
* feat: 절약 팁 조회와 북마크를 연동한다 * fix: 절약 팁 전체 목록을 조회한다
1 parent 4518dc2 commit d3138a9

13 files changed

Lines changed: 335 additions & 614 deletions

File tree

apps/web/app/(tabs)/benefits/_components/benefits-explorer.test.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { PolicyDetail, PolicySummary } from "@repo/schema/policy";
33
import { beforeEach, describe, expect, it, vi } from "vitest";
44
import { fetchSavedPolicies } from "@/api/bookmark";
55
import { bookmarkPolicy, fetchPolicies, fetchPolicyDetail, unbookmarkPolicy } from "@/api/policy";
6+
import { fetchAllSavingTips } from "@/api/tip";
67
import { POLICY_PAGE_SIZE } from "@/lib/queries/policy";
78
import { act, fireEvent, render, screen, waitFor } from "@/lib/test/react";
89
import { BenefitsExplorer } from "./benefits-explorer";
@@ -15,6 +16,12 @@ vi.mock("@/api/policy", () => ({
1516
}));
1617

1718
vi.mock("@/api/bookmark", () => ({ fetchSavedPolicies: vi.fn() }));
19+
vi.mock("@/api/tip", () => ({
20+
bookmarkSavingTip: vi.fn(),
21+
fetchAllSavingTips: vi.fn(),
22+
fetchSavingTips: vi.fn(),
23+
unbookmarkSavingTip: vi.fn(),
24+
}));
1825

1926
vi.mock("next/navigation", () => ({
2027
useSearchParams: () => new URLSearchParams(window.location.search),
@@ -69,6 +76,26 @@ beforeEach(() => {
6976
vi.mocked(fetchSavedPolicies).mockResolvedValue(SAVED);
7077
vi.mocked(bookmarkPolicy).mockResolvedValue(undefined);
7178
vi.mocked(unbookmarkPolicy).mockResolvedValue(undefined);
79+
vi.mocked(fetchAllSavingTips).mockResolvedValue([
80+
{
81+
bookmarked: false,
82+
category: "식비",
83+
description: "배달 메뉴 대신 집에서 직접 만드는 레시피 찾아보기",
84+
id: 1,
85+
sourceUrl: "https://www.youtube.com/watch?v=nZw2A76aZaw",
86+
subcategory: "배달음식",
87+
title: "집밥 레시피 활용팁",
88+
},
89+
{
90+
bookmarked: false,
91+
category: "생활",
92+
description: "중고구매 팁",
93+
id: 2,
94+
sourceUrl: "https://example.com/tip",
95+
subcategory: "의류",
96+
title: "당근마켓 중고활용팁",
97+
},
98+
]);
7299
window.history.replaceState(null, "", "/benefits");
73100
});
74101

@@ -407,14 +434,14 @@ describe("BenefitsExplorer", () => {
407434
expect(housingFilter).toHaveClass("text-gray-300", "focus-visible:ring-2");
408435
});
409436

410-
it("절약 팁 탭은 정적 목록을 보이고 정책 목록을 조회하지 않는다", async () => {
437+
it("절약 팁 탭은 API 목록을 보이고 정책 목록을 다시 조회하지 않는다", async () => {
411438
render(<BenefitsExplorer />);
412439
await screen.findByText("혜택 1");
413440
vi.mocked(fetchPolicies).mockClear();
414441

415442
fireEvent.click(screen.getByRole("tab", { name: "절약 팁" }));
416443

417-
expect(screen.getByText("집밥 레시피 활용팁")).toBeInTheDocument();
444+
expect(await screen.findByText("집밥 레시피 활용팁")).toBeInTheDocument();
418445
expect(
419446
screen.getByText("배달 메뉴 대신 집에서 직접 만드는 레시피 찾아보기"),
420447
).toBeInTheDocument();
@@ -424,17 +451,17 @@ describe("BenefitsExplorer", () => {
424451
);
425452
expect(screen.queryByText("혜택 1")).not.toBeInTheDocument();
426453
expect(fetchPolicies).not.toHaveBeenCalled();
454+
expect(fetchAllSavingTips).toHaveBeenCalledWith(null, 100);
427455
});
428456

429-
it("절약 팁은 미션과 같은 대분류로 목록을 좁힌다", async () => {
457+
it("절약 팁은 미션과 같은 대분류로 API 목록을 좁힌다", async () => {
430458
render(<BenefitsExplorer />);
431459
await screen.findByText("혜택 1");
432460

433461
fireEvent.click(screen.getByRole("tab", { name: "절약 팁" }));
434462
fireEvent.click(screen.getByRole("button", { name: "생활" }));
435463

436-
expect(screen.getByText("당근마켓 중고활용팁")).toBeInTheDocument();
437-
expect(screen.queryByText("집밥 레시피 활용팁")).not.toBeInTheDocument();
464+
await waitFor(() => expect(fetchAllSavingTips).toHaveBeenCalledWith("생활", 100));
438465
expect(screen.getByRole("button", { name: "생활" })).toHaveAttribute("aria-pressed", "true");
439466
});
440467

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,78 @@
1+
import type { SavingTipSummary } from "@repo/schema/tip";
12
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { bookmarkSavingTip, fetchAllSavingTips, unbookmarkSavingTip } from "@/api/tip";
24
import { fireEvent, render, screen, waitFor } from "@/lib/test/react";
5+
import { SavingTipList } from "./saving-tip-list";
36

4-
const bookmarkTip = vi.fn();
5-
const fetchTips = vi.fn();
6-
const unbookmarkTip = vi.fn();
7-
8-
vi.mock("@repo/bridge", () => ({ isNativeApp: () => false }));
9-
vi.mock("@/lib/open-external", () => ({ openExternalLink: vi.fn() }));
107
vi.mock("@/api/tip", () => ({
11-
bookmarkTip: (id: number) => bookmarkTip(id),
12-
fetchTips: () => fetchTips(),
13-
unbookmarkTip: (id: number) => unbookmarkTip(id),
8+
bookmarkSavingTip: vi.fn(),
9+
fetchAllSavingTips: vi.fn(),
10+
fetchSavingTips: vi.fn(),
11+
unbookmarkSavingTip: vi.fn(),
1412
}));
1513

16-
import { SavingTipList } from "./saving-tip-list";
14+
const TIPS: SavingTipSummary[] = [
15+
{
16+
bookmarked: false,
17+
category: "식비",
18+
description: "배달 메뉴 대신 집에서 직접 만드는 레시피 찾아보기",
19+
id: 1,
20+
sourceUrl: "https://www.youtube.com/watch?v=nZw2A76aZaw",
21+
subcategory: "배달음식",
22+
title: "집밥 레시피 활용팁",
23+
},
24+
{
25+
bookmarked: true,
26+
category: "생활",
27+
description: "당근마켓서 중고구매하고 안 쓰는 옷은 판매하기",
28+
id: 2,
29+
sourceUrl: "https://example.com/tip",
30+
subcategory: "의류",
31+
title: "당근마켓 중고활용팁",
32+
},
33+
];
1734

1835
describe("SavingTipList", () => {
1936
beforeEach(() => {
2037
vi.clearAllMocks();
21-
fetchTips.mockResolvedValue([
22-
{ id: 101, title: "집밥 레시피 활용팁", category: "식비", bookmarked: false },
23-
{ id: 102, title: "밀프렙 식단관리팁", category: "식비", bookmarked: false },
24-
]);
25-
bookmarkTip.mockResolvedValue(undefined);
26-
unbookmarkTip.mockResolvedValue(undefined);
38+
vi.mocked(fetchAllSavingTips).mockResolvedValue(TIPS);
39+
vi.mocked(bookmarkSavingTip).mockResolvedValue(undefined);
40+
vi.mocked(unbookmarkSavingTip).mockResolvedValue(undefined);
2741
});
2842

29-
it("팁 북마크 API로 별 저장을 요청한다", async () => {
43+
it("서버에서 받은 팁과 원문 링크를 그린다", async () => {
3044
render(<SavingTipList />);
3145

32-
const star = await screen.findByRole("button", { name: "집밥 레시피 활용팁 저장" });
33-
await waitFor(() => expect(star).toHaveAttribute("aria-busy", "false"));
34-
fireEvent.click(star);
35-
36-
expect(star).toHaveAttribute("aria-pressed", "true");
37-
await waitFor(() => expect(bookmarkTip).toHaveBeenCalledWith(101));
46+
expect(await screen.findByText("집밥 레시피 활용팁")).toBeInTheDocument();
47+
expect(fetchAllSavingTips).toHaveBeenCalledWith(null, 100);
48+
expect(screen.getByRole("link", { name: / / })).toHaveAttribute(
49+
"href",
50+
"https://www.youtube.com/watch?v=nZw2A76aZaw",
51+
);
3852
});
3953

40-
it("저장됨 화면에서는 API가 저장됨으로 준 팁만 보여준다", async () => {
41-
fetchTips.mockResolvedValue([
42-
{ id: 101, title: "집밥 레시피 활용팁", category: "식비", bookmarked: false },
43-
{ id: 102, title: "밀프렙 식단관리팁", category: "식비", bookmarked: true },
44-
]);
54+
it("카테고리를 누르면 해당 카테고리 API를 다시 조회한다", async () => {
55+
render(<SavingTipList />);
56+
await screen.findByText("집밥 레시피 활용팁");
57+
58+
fireEvent.click(screen.getByRole("button", { name: "생활" }));
4559

60+
await waitFor(() => expect(fetchAllSavingTips).toHaveBeenCalledWith("생활", 100));
61+
});
62+
63+
it("저장한 팁만 저장 목록에 남긴다", async () => {
4664
render(<SavingTipList savedOnly />);
4765

48-
expect(await screen.findByText("밀프렙 식단관리팁")).toBeInTheDocument();
66+
expect(await screen.findByText("당근마켓 중고활용팁")).toBeInTheDocument();
4967
expect(screen.queryByText("집밥 레시피 활용팁")).not.toBeInTheDocument();
5068
});
5169

52-
it("북마크 요청이 끝날 때까지 같은 팁을 다시 토글하지 않는다", async () => {
53-
let completeSave: (() => void) | undefined;
54-
bookmarkTip.mockImplementation(
55-
() =>
56-
new Promise<void>((resolve) => {
57-
completeSave = resolve;
58-
}),
59-
);
70+
it("별을 누르면 서버 북마크 API를 호출한다", async () => {
6071
render(<SavingTipList />);
72+
await screen.findByText("집밥 레시피 활용팁");
6173

62-
const star = await screen.findByRole("button", { name: "집밥 레시피 활용팁 저장" });
63-
await waitFor(() => expect(star).toHaveAttribute("aria-busy", "false"));
64-
fireEvent.click(star);
65-
fireEvent.click(star);
74+
fireEvent.click(screen.getByRole("button", { name: "집밥 레시피 활용팁 저장" }));
6675

67-
await waitFor(() => expect(bookmarkTip).toHaveBeenCalledTimes(1));
68-
completeSave?.();
69-
await waitFor(() => expect(star).toHaveAttribute("aria-busy", "false"));
76+
await waitFor(() => expect(bookmarkSavingTip).toHaveBeenCalledWith(1));
7077
});
7178
});

0 commit comments

Comments
 (0)