-
Notifications
You must be signed in to change notification settings - Fork 6
Feat : 즐겨찾기 기능 구현 #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1d01dc
18ae882
3acf40c
e5b76ff
4f79654
1de5822
c886a21
4ee35f6
3cce7a2
e5dd248
0a2768f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 && ( | ||
| <div | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 케밥 버튼과 즐겨찾기 버튼이 isFavoritePage이냐 아니냐에 따라서 조건부 렌더링이 되니
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 리팩토링 때 진행해보겠습니다~! |
||
| onClick={() => setIsSubscribed(!isSubscribed)} | ||
| onClick={handleFavoriteToggle} | ||
| className="absolute top-[15px] right-[15px] z-1" | ||
| > | ||
| <Image | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
| <> | ||
| <div className="bg-gray100 w-full h-[219px] flex justify-center items-center"> | ||
|
|
@@ -91,14 +83,7 @@ const LinkPage = ({ | |
| </div> | ||
| <CardsLayout> | ||
| {linkCardList.map((link) => ( | ||
| <LinkCard | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트가 많이 가벼워진게 보기 좋네요! |
||
| key={link.id} | ||
| openEdit={() => handleModalOpen("EditLink", link.url, link.id)} | ||
| openDelete={() => | ||
| handleModalOpen("DeleteLinkModal", link.url, link.id) | ||
| } | ||
| info={link} | ||
| /> | ||
| <LinkCard key={link.id} info={link} /> | ||
| ))} | ||
| </CardsLayout> | ||
| <Pagination totalCount={totalCount} /> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분도 토스트 적용하는게 좋을까요? (즐겨찾기 추가/실패)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분보다는 드롭다운에 수정하기, 삭제하기 했을 때 토스팅 적용이 더 나을 것 같습니다!