Skip to content

Commit ccf3af7

Browse files
authored
Merge pull request #94 from codeit9-temporary/feature/link-favorite
Feat : 즐겨찾기 기능 구현
2 parents d9ea590 + 0a2768f commit ccf3af7

File tree

6 files changed

+140
-48
lines changed

6 files changed

+140
-48
lines changed

components/Link/LinkCard.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useEffect, useState } from "react";
22
import { useRouter } from "next/router";
3+
import { putLinkFavorite } from "@/lib/api/link";
4+
import { useLinkCardStore } from "@/store/useLinkCardStore";
35
import timeAgo from "@/util/timAgo";
46
import Image from "next/image";
57
import Dropdown from "../Dropdown";
@@ -15,14 +17,13 @@ interface LinkCardProps {
1517
url: string;
1618
createdAt: string;
1719
};
18-
openEdit?: () => void;
19-
openDelete?: () => void;
2020
}
2121

22-
const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => {
23-
const [isSubscribed, setIsSubscribed] = useState(false);
22+
const LinkCard = ({ info }: LinkCardProps) => {
23+
const [isSubscribed, setIsSubscribed] = useState(info.favorite || false);
2424
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
25-
const { isOpen: isModalOpen } = useModalStore();
25+
const { isOpen, openModal } = useModalStore();
26+
const { updateFavorite } = useLinkCardStore();
2627

2728
const formattedDate = info.createdAt?.slice(0, 10).replace(/-/g, ".");
2829
const createdTime = timeAgo(info.createdAt);
@@ -32,20 +33,39 @@ const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => {
3233

3334
// 모달이 열릴 때 드롭다운 닫기
3435
useEffect(() => {
35-
if (isModalOpen) setIsDropdownOpen(false);
36-
}, [isModalOpen]);
36+
if (isOpen) setIsDropdownOpen(false);
37+
}, [isOpen]);
38+
39+
// 즐겨찾기 버튼 클릭 시 호출되는 함수
40+
const handleFavoriteToggle = async () => {
41+
setIsSubscribed((prev) => !prev);
42+
try {
43+
await putLinkFavorite(info.id, { favorite: !isSubscribed });
44+
updateFavorite(info.id, !isSubscribed);
45+
} catch (error) {
46+
console.error("즐겨찾기 설정 중 오류 발생:", error);
47+
}
48+
};
3749

3850
// dropdown 버튼
3951
const toggleDropdown = () => setIsDropdownOpen((prev) => !prev);
4052

53+
const handleModalOpen = (
54+
type: "EditLink" | "DeleteLinkModal",
55+
link: string,
56+
linkId: number
57+
) => {
58+
openModal(type, { link, linkId });
59+
};
60+
4161
const dropdownItems = [
4262
{
4363
label: "수정하기",
44-
onClick: openEdit,
64+
onClick: () => handleModalOpen("EditLink", info.url, info.id),
4565
},
4666
{
4767
label: "삭제하기",
48-
onClick: openDelete,
68+
onClick: () => handleModalOpen("DeleteLinkModal", info.url, info.id),
4969
},
5070
];
5171

@@ -61,7 +81,7 @@ const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => {
6181
{/* 즐겨찾기 페이지가 아닐 때에는 즐겨찾기 버튼 렌더링x */}
6282
{!isFavoritePage && (
6383
<div
64-
onClick={() => setIsSubscribed(!isSubscribed)}
84+
onClick={handleFavoriteToggle}
6585
className="absolute top-[15px] right-[15px] z-1"
6686
>
6787
<Image

lib/api/link.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface putLinkURLProps {
99
url: string;
1010
}
1111

12-
interface putFolderNameProps {
12+
interface putLinkFavoriteProps {
1313
favorite: boolean;
1414
}
1515

@@ -79,12 +79,12 @@ export const deleteLinkURL = async (linkId: number) => {
7979
};
8080

8181
// 링크의 즐겨찾기 설정(auth)
82-
export const putFolderName = async (
82+
export const putLinkFavorite = async (
8383
linkId: number,
84-
body: putFolderNameProps
84+
body: putLinkFavoriteProps
8585
) => {
8686
try {
87-
const res = await proxy.put(`/api/links/${linkId}`, body);
87+
const res = await proxy.put(`/api/links/${linkId}/favorite`, body);
8888
if (res.status >= 200 && res.status < 300) return res.data;
8989
} catch (err) {
9090
console.error("에러 메시지: ", err instanceof Error ? err.message : err);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import axiosInstance from "@/lib/api/axiosInstanceApi";
2+
import { NextApiRequest, NextApiResponse } from "next";
3+
import { isAxiosError } from "axios";
4+
5+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
6+
const token = req.cookies.accessToken;
7+
8+
if (!token) {
9+
return res.status(401).json({ error: "사용자 정보를 찾을 수 없습니다." });
10+
}
11+
12+
const { linkId } = req.query;
13+
14+
// 링크 즐겨 찾기
15+
switch (req.method) {
16+
case "PUT":
17+
const { favorite } = req.body;
18+
if (favorite === undefined) {
19+
return res.status(400).json({ message: "즐겨찾기 상태가 필요합니다." });
20+
}
21+
22+
try {
23+
await axiosInstance.put(
24+
`/links/${linkId}/favorite`,
25+
{ favorite },
26+
{
27+
headers: {
28+
Authorization: `Bearer ${token}`,
29+
},
30+
}
31+
);
32+
33+
return res.status(200).json({
34+
message: `링크 즐겨찾기 ${favorite ? "추가" : "삭제"} 성공`,
35+
});
36+
} catch (error) {
37+
if (isAxiosError(error) && error.response) {
38+
const status = error.response.status;
39+
const message =
40+
error.response.data?.message || "알 수 없는 오류 발생";
41+
return res.status(status).json({ message });
42+
}
43+
return res.status(500).json({ message: "서버 오류" });
44+
}
45+
46+
default:
47+
return res.status(405).json({ message: "허용되지 않은 접근 방법" });
48+
}
49+
};
50+
51+
export default handler;
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
3737
}
3838
return res.status(500).json({ message: "서버 오류" });
3939
}
40+
4041
// 링크 수정
4142
case "PUT":
4243
if (!linkId) {
@@ -53,13 +54,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
5354
}
5455

5556
try {
56-
await axiosInstance.put(`/links/${linkId}`, updateData, {
57-
headers: {
58-
Authorization: `Bearer ${token}`,
59-
},
60-
});
57+
const updatedLink = await axiosInstance.put(
58+
`/links/${linkId}`,
59+
updateData,
60+
{
61+
headers: {
62+
Authorization: `Bearer ${token}`,
63+
},
64+
}
65+
);
6166

62-
return res.status(200).json({ message: "링크 업데이트 성공" });
67+
return res.status(200).json(updatedLink.data);
6368
} catch (error) {
6469
if (isAxiosError(error) && error.response) {
6570
const status = error.response.status;

pages/link/index.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ const LinkPage = ({
5252
}: LinkPageProps) => {
5353
const router = useRouter();
5454
const { search } = router.query;
55-
const { isOpen, openModal } = useModalStore();
5655
const { linkCardList, setLinkCardList } = useLinkCardStore();
56+
const { isOpen } = useModalStore();
5757
const [folderList, setFolderList] = useState(initialFolderList);
5858

5959
// 링크페이지의 query가 바뀌면 새로운 리스트로 업데이트 해주는 훅
@@ -64,14 +64,6 @@ const LinkPage = ({
6464
setLinkCardList(initialLinkList);
6565
}, [initialLinkList, setLinkCardList]);
6666

67-
const handleModalOpen = (
68-
type: "EditLink" | "DeleteLinkModal",
69-
link: string,
70-
linkId: number
71-
) => {
72-
openModal(type, { link, linkId });
73-
};
74-
7567
return (
7668
<>
7769
<div className="bg-gray100 w-full h-[219px] flex justify-center items-center">
@@ -91,14 +83,7 @@ const LinkPage = ({
9183
</div>
9284
<CardsLayout>
9385
{linkCardList.map((link) => (
94-
<LinkCard
95-
key={link.id}
96-
openEdit={() => handleModalOpen("EditLink", link.url, link.id)}
97-
openDelete={() =>
98-
handleModalOpen("DeleteLinkModal", link.url, link.id)
99-
}
100-
info={link}
101-
/>
86+
<LinkCard key={link.id} info={link} />
10287
))}
10388
</CardsLayout>
10489
<Pagination totalCount={totalCount} />

store/useLinkCardStore.tsx

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { deleteLinkURL, getLinks, putLinkURL } from "@/lib/api/link";
1+
import {
2+
deleteLinkURL,
3+
getLinks,
4+
putLinkFavorite,
5+
putLinkURL,
6+
} from "@/lib/api/link";
27
import { create } from "zustand";
38
import { LinkData } from "@/types/linkTypes";
49

@@ -11,6 +16,7 @@ interface LinkCardStore {
1116
setLinkCardList: (list: LinkData[]) => void;
1217
updateLink: (linkId: number, body: UpdateLinkBody) => Promise<void>;
1318
deleteLink: (linkId: number) => Promise<void>;
19+
updateFavorite: (linkId: number, favorite: boolean) => Promise<void>;
1420
}
1521

1622
export const useLinkCardStore = create<LinkCardStore>((set) => ({
@@ -23,13 +29,18 @@ export const useLinkCardStore = create<LinkCardStore>((set) => ({
2329
// 수정 요청 보낸 후 목록 가져오기
2430
updateLink: async (linkId: number, body: UpdateLinkBody) => {
2531
try {
26-
await putLinkURL(linkId, body);
32+
// 프록시 서버에서 수정된 링크 데이터를 받아옴
33+
const updatedData = await putLinkURL(linkId, body);
2734

28-
const res = await getLinks();
29-
const updatedList = res.list;
30-
31-
// 상태 업데이트
32-
set({ linkCardList: updatedList });
35+
// 수정된 데이터를 사용하여 상태 업데이트
36+
if (updatedData) {
37+
set((state) => {
38+
const updatedList = state.linkCardList.map((link) =>
39+
link.id === linkId ? { ...link, ...updatedData } : link
40+
);
41+
return { linkCardList: updatedList };
42+
});
43+
}
3344
} catch (error) {
3445
console.error("삭제 중 오류 발생:", error);
3546
}
@@ -39,13 +50,33 @@ export const useLinkCardStore = create<LinkCardStore>((set) => ({
3950
deleteLink: async (linkId: number) => {
4051
try {
4152
await deleteLinkURL(linkId);
42-
const res = await getLinks();
43-
const updatedList = res.list;
4453

45-
// 상태 업데이트
46-
set({ linkCardList: updatedList });
54+
// 삭제된 항목을 제외한 나머지 항목으로 상태 업데이트
55+
set((state) => {
56+
const updatedList = state.linkCardList.filter(
57+
(link) => link.id !== linkId
58+
);
59+
return { linkCardList: updatedList };
60+
});
4761
} catch (error) {
4862
console.error("삭제 중 오류 발생:", error);
4963
}
5064
},
65+
66+
// 즐겨찾기 상태 업데이트
67+
updateFavorite: async (linkId: number, favorite: boolean) => {
68+
try {
69+
await putLinkFavorite(linkId, { favorite });
70+
71+
// 변경된 항목만 상태 업데이트
72+
set((state) => {
73+
const updatedList = state.linkCardList.map((link) =>
74+
link.id === linkId ? { ...link, favorite } : link
75+
);
76+
return { linkCardList: updatedList };
77+
});
78+
} catch (error) {
79+
console.error("즐겨찾기 상태 업데이트 중 오류 발생:", error);
80+
}
81+
},
5182
}));

0 commit comments

Comments
 (0)