diff --git a/components/Link/LinkCard.tsx b/components/Link/LinkCard.tsx index 2b452ea..31abdf7 100644 --- a/components/Link/LinkCard.tsx +++ b/components/Link/LinkCard.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import { putLinkFavorite } from "@/lib/api/link"; +import { useLinkCardStore } from "@/store/useLinkCardStore"; import timeAgo from "@/util/timAgo"; import Image from "next/image"; import Dropdown from "../Dropdown"; @@ -15,14 +17,13 @@ interface LinkCardProps { url: string; createdAt: string; }; - openEdit?: () => void; - openDelete?: () => void; } -const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => { - const [isSubscribed, setIsSubscribed] = useState(false); +const LinkCard = ({ info }: LinkCardProps) => { + const [isSubscribed, setIsSubscribed] = useState(info.favorite || false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const { isOpen: isModalOpen } = useModalStore(); + const { isOpen, openModal } = useModalStore(); + const { updateFavorite } = useLinkCardStore(); const formattedDate = info.createdAt?.slice(0, 10).replace(/-/g, "."); const createdTime = timeAgo(info.createdAt); @@ -32,20 +33,39 @@ const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => { // 모달이 열릴 때 드롭다운 닫기 useEffect(() => { - if (isModalOpen) setIsDropdownOpen(false); - }, [isModalOpen]); + if (isOpen) setIsDropdownOpen(false); + }, [isOpen]); + + // 즐겨찾기 버튼 클릭 시 호출되는 함수 + const handleFavoriteToggle = async () => { + setIsSubscribed((prev) => !prev); + try { + await putLinkFavorite(info.id, { favorite: !isSubscribed }); + updateFavorite(info.id, !isSubscribed); + } catch (error) { + console.error("즐겨찾기 설정 중 오류 발생:", error); + } + }; // dropdown 버튼 const toggleDropdown = () => setIsDropdownOpen((prev) => !prev); + const handleModalOpen = ( + type: "EditLink" | "DeleteLinkModal", + link: string, + linkId: number + ) => { + openModal(type, { link, linkId }); + }; + const dropdownItems = [ { label: "수정하기", - onClick: openEdit, + onClick: () => handleModalOpen("EditLink", info.url, info.id), }, { label: "삭제하기", - onClick: openDelete, + onClick: () => handleModalOpen("DeleteLinkModal", info.url, info.id), }, ]; @@ -61,7 +81,7 @@ const LinkCard = ({ openEdit, openDelete, info }: LinkCardProps) => { {/* 즐겨찾기 페이지가 아닐 때에는 즐겨찾기 버튼 렌더링x */} {!isFavoritePage && (
setIsSubscribed(!isSubscribed)} + onClick={handleFavoriteToggle} className="absolute top-[15px] right-[15px] z-1" > { }; // 링크의 즐겨찾기 설정(auth) -export const putFolderName = async ( +export const putLinkFavorite = async ( linkId: number, - body: putFolderNameProps + body: putLinkFavoriteProps ) => { try { - const res = await proxy.put(`/api/links/${linkId}`, body); + const res = await proxy.put(`/api/links/${linkId}/favorite`, body); if (res.status >= 200 && res.status < 300) return res.data; } catch (err) { console.error("에러 메시지: ", err instanceof Error ? err.message : err); diff --git a/pages/api/links/[linkId]/favorite.ts b/pages/api/links/[linkId]/favorite.ts new file mode 100644 index 0000000..7dc0322 --- /dev/null +++ b/pages/api/links/[linkId]/favorite.ts @@ -0,0 +1,51 @@ +import axiosInstance from "@/lib/api/axiosInstanceApi"; +import { NextApiRequest, NextApiResponse } from "next"; +import { isAxiosError } from "axios"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const token = req.cookies.accessToken; + + if (!token) { + return res.status(401).json({ error: "사용자 정보를 찾을 수 없습니다." }); + } + + const { linkId } = req.query; + + // 링크 즐겨 찾기 + switch (req.method) { + case "PUT": + const { favorite } = req.body; + if (favorite === undefined) { + return res.status(400).json({ message: "즐겨찾기 상태가 필요합니다." }); + } + + try { + await axiosInstance.put( + `/links/${linkId}/favorite`, + { favorite }, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + return res.status(200).json({ + message: `링크 즐겨찾기 ${favorite ? "추가" : "삭제"} 성공`, + }); + } catch (error) { + if (isAxiosError(error) && error.response) { + const status = error.response.status; + const message = + error.response.data?.message || "알 수 없는 오류 발생"; + return res.status(status).json({ message }); + } + return res.status(500).json({ message: "서버 오류" }); + } + + default: + return res.status(405).json({ message: "허용되지 않은 접근 방법" }); + } +}; + +export default handler; diff --git a/pages/api/links/[linkId].tsx b/pages/api/links/[linkId]/index.ts similarity index 88% rename from pages/api/links/[linkId].tsx rename to pages/api/links/[linkId]/index.ts index ae79ddc..403ea34 100644 --- a/pages/api/links/[linkId].tsx +++ b/pages/api/links/[linkId]/index.ts @@ -37,6 +37,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } return res.status(500).json({ message: "서버 오류" }); } + // 링크 수정 case "PUT": if (!linkId) { @@ -53,13 +54,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } try { - await axiosInstance.put(`/links/${linkId}`, updateData, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const updatedLink = await axiosInstance.put( + `/links/${linkId}`, + updateData, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); - return res.status(200).json({ message: "링크 업데이트 성공" }); + return res.status(200).json(updatedLink.data); } catch (error) { if (isAxiosError(error) && error.response) { const status = error.response.status; diff --git a/pages/link/index.tsx b/pages/link/index.tsx index 65884e9..8f18dd9 100644 --- a/pages/link/index.tsx +++ b/pages/link/index.tsx @@ -52,8 +52,8 @@ const LinkPage = ({ }: LinkPageProps) => { const router = useRouter(); const { search } = router.query; - const { isOpen, openModal } = useModalStore(); const { linkCardList, setLinkCardList } = useLinkCardStore(); + const { isOpen } = useModalStore(); const [folderList, setFolderList] = useState(initialFolderList); // 링크페이지의 query가 바뀌면 새로운 리스트로 업데이트 해주는 훅 @@ -64,14 +64,6 @@ const LinkPage = ({ setLinkCardList(initialLinkList); }, [initialLinkList, setLinkCardList]); - const handleModalOpen = ( - type: "EditLink" | "DeleteLinkModal", - link: string, - linkId: number - ) => { - openModal(type, { link, linkId }); - }; - return ( <>
@@ -91,14 +83,7 @@ const LinkPage = ({
{linkCardList.map((link) => ( - handleModalOpen("EditLink", link.url, link.id)} - openDelete={() => - handleModalOpen("DeleteLinkModal", link.url, link.id) - } - info={link} - /> + ))} diff --git a/store/useLinkCardStore.tsx b/store/useLinkCardStore.tsx index 40b095b..ef5b92f 100644 --- a/store/useLinkCardStore.tsx +++ b/store/useLinkCardStore.tsx @@ -1,4 +1,9 @@ -import { deleteLinkURL, getLinks, putLinkURL } from "@/lib/api/link"; +import { + deleteLinkURL, + getLinks, + putLinkFavorite, + putLinkURL, +} from "@/lib/api/link"; import { create } from "zustand"; import { LinkData } from "@/types/linkTypes"; @@ -11,6 +16,7 @@ interface LinkCardStore { setLinkCardList: (list: LinkData[]) => void; updateLink: (linkId: number, body: UpdateLinkBody) => Promise; deleteLink: (linkId: number) => Promise; + updateFavorite: (linkId: number, favorite: boolean) => Promise; } export const useLinkCardStore = create((set) => ({ @@ -23,13 +29,18 @@ export const useLinkCardStore = create((set) => ({ // 수정 요청 보낸 후 목록 가져오기 updateLink: async (linkId: number, body: UpdateLinkBody) => { try { - await putLinkURL(linkId, body); + // 프록시 서버에서 수정된 링크 데이터를 받아옴 + const updatedData = await putLinkURL(linkId, body); - const res = await getLinks(); - const updatedList = res.list; - - // 상태 업데이트 - set({ linkCardList: updatedList }); + // 수정된 데이터를 사용하여 상태 업데이트 + if (updatedData) { + set((state) => { + const updatedList = state.linkCardList.map((link) => + link.id === linkId ? { ...link, ...updatedData } : link + ); + return { linkCardList: updatedList }; + }); + } } catch (error) { console.error("삭제 중 오류 발생:", error); } @@ -39,13 +50,33 @@ export const useLinkCardStore = create((set) => ({ deleteLink: async (linkId: number) => { try { await deleteLinkURL(linkId); - const res = await getLinks(); - const updatedList = res.list; - // 상태 업데이트 - set({ linkCardList: updatedList }); + // 삭제된 항목을 제외한 나머지 항목으로 상태 업데이트 + set((state) => { + const updatedList = state.linkCardList.filter( + (link) => link.id !== linkId + ); + return { linkCardList: updatedList }; + }); } catch (error) { console.error("삭제 중 오류 발생:", error); } }, + + // 즐겨찾기 상태 업데이트 + updateFavorite: async (linkId: number, favorite: boolean) => { + try { + await putLinkFavorite(linkId, { favorite }); + + // 변경된 항목만 상태 업데이트 + set((state) => { + const updatedList = state.linkCardList.map((link) => + link.id === linkId ? { ...link, favorite } : link + ); + return { linkCardList: updatedList }; + }); + } catch (error) { + console.error("즐겨찾기 상태 업데이트 중 오류 발생:", error); + } + }, }));