Skip to content

Commit 27faf78

Browse files
authored
Merge pull request #66 from codeit9-temporary/feature/dropdown
Feat : 링크 수정 & 삭제 기능 작업
2 parents 3e3d93f + 1ea09c8 commit 27faf78

File tree

12 files changed

+342
-47
lines changed

12 files changed

+342
-47
lines changed

components/Dropdown.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from "react";
2+
3+
interface DropdownProps {
4+
onEdit?: () => void;
5+
openDelete?: () => void;
6+
}
7+
8+
const Dropdown = ({ onEdit, openDelete }: DropdownProps) => {
9+
const buttonStyle =
10+
"block w-full py-2 text-sm hover:bg-gray200 hover:text-purple100";
11+
12+
return (
13+
<div className="absolute top-[17px] right-0 flex flex-col gap-[2px] min-w-[100px] bg-white shadow-lg rounded">
14+
<button className={buttonStyle} onClick={onEdit}>
15+
수정하기
16+
</button>
17+
<button className={buttonStyle} onClick={openDelete}>
18+
삭제하기
19+
</button>
20+
</div>
21+
);
22+
};
23+
24+
export default Dropdown;

components/LinkCard.tsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
2+
import { useRouter } from "next/router";
23
import timeAgo from "@/util/timAgo";
34
import Image from "next/image";
5+
import Dropdown from "./Dropdown";
6+
import useModalStore from "@/store/useModalStore";
47

58
interface LinkCardProps {
69
info: {
@@ -12,16 +15,29 @@ interface LinkCardProps {
1215
url: string;
1316
createdAt: string;
1417
};
15-
isFavoritePage?: boolean;
18+
onEdit?: () => void;
19+
openDelete?: () => void;
1620
}
1721

18-
const LinkCard = ({ isFavoritePage, info }: LinkCardProps) => {
22+
const LinkCard = ({ onEdit, openDelete, info }: LinkCardProps) => {
1923
const [isSubscribed, setIsSubscribed] = useState(false);
20-
const [isOpen, setIsOpen] = useState(false);
24+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
25+
const { isOpen: isModalOpen } = useModalStore(); // 모달 열림 상태 구독
2126

2227
const formattedDate = info.createdAt?.slice(0, 10).replace(/-/g, ".");
2328
const createdTime = timeAgo(info.createdAt);
2429

30+
const router = useRouter();
31+
const isFavoritePage = router.pathname === "/favorite";
32+
33+
// 모달이 열릴 때 드롭다운 닫기
34+
useEffect(() => {
35+
if (isModalOpen) setIsDropdownOpen(false);
36+
}, [isModalOpen]);
37+
38+
// dropdown 버튼
39+
const toggleDropdown = () => setIsDropdownOpen((prev) => !prev);
40+
2541
return (
2642
<div className="w-[340px] h-[344px] rounded-[12px] shadow-lg overflow-hidden cursor-pointer hover:scale-105 hover:duration-300">
2743
<section className="relative w-full h-[60%]">
@@ -31,47 +47,41 @@ const LinkCard = ({ isFavoritePage, info }: LinkCardProps) => {
3147
alt="링크 미리보기"
3248
fill
3349
/>
34-
{/* isFavoritePage가 false일 때만 즐겨찾기 버튼 렌더링 */}
35-
{!isFavoritePage &&
36-
(isSubscribed ? (
37-
<div
38-
onClick={() => setIsSubscribed(!isSubscribed)}
39-
className="absolute top-[15px] right-[15px] z-1"
40-
>
41-
<Image
42-
src="/icons/star-fill.svg"
43-
width={32}
44-
height={32}
45-
alt="subscripe button"
46-
/>
47-
</div>
48-
) : (
49-
<div
50-
onClick={() => setIsSubscribed(!isSubscribed)}
51-
className="absolute top-[15px] right-[15px] z-1"
52-
>
53-
<Image
54-
src="/icons/star-empty.svg"
55-
width={32}
56-
height={32}
57-
alt="subscripe button"
58-
/>
59-
</div>
60-
))}
50+
{/* isFavoritePage일 때만 즐겨찾기 버튼 렌더링 */}
51+
{!isFavoritePage && (
52+
<div
53+
onClick={() => setIsSubscribed(!isSubscribed)}
54+
className="absolute top-[15px] right-[15px] z-1"
55+
>
56+
<Image
57+
src={
58+
isSubscribed ? "/icons/star-fill.svg" : "/icons/star-empty.svg"
59+
}
60+
width={32}
61+
height={32}
62+
alt="subscribe button"
63+
/>
64+
</div>
65+
)}
6166
</section>
6267

6368
<section className="w-full h-[40%] flex flex-col justify-between gap-[10px] pt-[15px] px-[20px] pb-[10px]">
6469
<div className="flex justify-between">
6570
<span className="text-sm text-gray-400">
6671
{createdTime || "1일 전"}
6772
</span>
68-
{/* isFavoritePage가 false일 때만 케밥 버튼 렌더링 */}
73+
{/* isFavoritePage일 때만 케밥 버튼 렌더링 */}
6974
{!isFavoritePage && (
70-
<div
71-
className="relative w-[21px] h-[17px]"
72-
onClick={(state) => setIsOpen(!state)}
73-
>
74-
<Image src="/icons/kebab.svg" alt="kebab button" fill />
75+
<div className="relative">
76+
<button
77+
className="relative w-[21px] h-[17px]"
78+
onClick={toggleDropdown}
79+
>
80+
<Image src="/icons/kebab.svg" alt="kebab button" fill />
81+
</button>
82+
{isDropdownOpen && (
83+
<Dropdown onEdit={onEdit} openDelete={openDelete} />
84+
)}
7585
</div>
7686
)}
7787
</div>

components/modal/AddFolderModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeEvent, useState } from "react";
2+
import { postFolders } from "@/lib/api/folder";
23
import ModalContainer from "./modalComponents/ModalContainer";
34
import ModalInput from "./modalComponents/ModalInput";
4-
import { postFolders } from "@/lib/api/folder";
55
import useModalStore from "@/store/useModalStore";
66
import SubmitButton from "../SubMitButton";
77

components/modal/DeleteLinkModal.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
1+
import useModalStore from "@/store/useModalStore";
12
import SubmitButton from "../SubMitButton";
23
import ModalContainer from "./modalComponents/ModalContainer";
4+
import { useLinkCardStore } from "@/store/useLinkCardStore";
5+
6+
const DeleteLinkModal = ({
7+
link,
8+
linkId,
9+
}: {
10+
link: string;
11+
linkId: number;
12+
}) => {
13+
const { closeModal } = useModalStore();
14+
const { deleteLink } = useLinkCardStore();
15+
16+
const handleDelete = async () => {
17+
try {
18+
await deleteLink(linkId);
19+
closeModal();
20+
} catch (error) {
21+
console.error("Failed to delete the link:", error);
22+
}
23+
};
324

4-
const DeleteLinkModal = ({ link }: { link: string }) => {
525
return (
626
<ModalContainer title="링크 삭제" subtitle={link}>
727
<SubmitButton
828
type="button"
9-
// onClick={handleSubmit}
29+
onClick={handleDelete}
1030
width="w-full"
11-
height="h-[51px] "
31+
height="h-[51px]"
1232
color="negative"
1333
>
1434
삭제하기

components/modal/EditLink.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ChangeEvent, useState } from "react";
2+
import { useLinkCardStore } from "@/store/useLinkCardStore";
3+
import ModalContainer from "./modalComponents/ModalContainer";
4+
import ModalInput from "./modalComponents/ModalInput";
5+
import useModalStore from "@/store/useModalStore";
6+
import SubmitButton from "../SubMitButton";
7+
8+
const EditLink = ({
9+
folderName,
10+
link,
11+
linkId,
12+
}: {
13+
folderName: string;
14+
link: string;
15+
linkId: number;
16+
}) => {
17+
const [value, setValue] = useState("");
18+
const { closeModal } = useModalStore();
19+
const { updateLink } = useLinkCardStore();
20+
21+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
22+
setValue(e.target.value);
23+
};
24+
25+
const handleSubmit = async () => {
26+
const body = {
27+
url: value,
28+
};
29+
if (value !== "") {
30+
await updateLink(linkId, body);
31+
}
32+
closeModal();
33+
};
34+
return (
35+
<ModalContainer title="링크 주소 변경">
36+
<ModalInput
37+
placeholder={link}
38+
name={folderName}
39+
value={value}
40+
onChange={handleChange}
41+
/>
42+
<SubmitButton
43+
type="button"
44+
onClick={handleSubmit}
45+
width="w-full"
46+
height="h-[51px] "
47+
color="positive"
48+
>
49+
변경하기
50+
</SubmitButton>
51+
</ModalContainer>
52+
);
53+
};
54+
export default EditLink;

components/modal/modalManager/ModalManager.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import AddFolderModal from "../AddFolderModal";
55
import DeleteLinkModal from "../DeleteLinkModal";
66
import EditModal from "../EditModal";
77
import SNSModal from "../SNSModal";
8+
import EditLink from "../EditLink";
89

910
export const ModalType = {
1011
AddFolderModal: "AddFolderModal",
@@ -13,6 +14,7 @@ export const ModalType = {
1314
DeleteLinkModal: "DeleteLinkModal",
1415
EditModal: "EditModal",
1516
SNSModal: "SNSModal",
17+
EditLink: "EditLink",
1618
} as const;
1719

1820
export type ModalKeysType = keyof typeof ModalType;
@@ -42,10 +44,23 @@ export const Modal = () => {
4244
case "DeleteFolderModal":
4345
return <DeleteFolderModal folderName={props.folderName || "폴더이름"} />;
4446
case "DeleteLinkModal":
45-
return <DeleteLinkModal link={props.link || "링크"} />;
47+
return (
48+
<DeleteLinkModal
49+
link={props.link || "링크"}
50+
linkId={Number(props.linkId)}
51+
/>
52+
);
4653
case "EditModal":
4754
return <EditModal folderName={props.folderName || "폴더이름"} />;
4855
case "SNSModal":
4956
return <SNSModal folderName={props.folderName || "폴더이름"} />;
57+
case "EditLink":
58+
return (
59+
<EditLink
60+
folderName={props.folderName || "폴더이름"}
61+
link={props.link || "링크"}
62+
linkId={Number(props.linkId)}
63+
/>
64+
);
5065
}
5166
};

pages/api/links/[linkId].tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
switch (req.method) {
15+
// 링크 삭제
16+
case "DELETE":
17+
if (!linkId) {
18+
return res
19+
.status(400)
20+
.json({ message: "삭제할 링크 ID가 필요합니다." });
21+
}
22+
23+
try {
24+
await axiosInstance.delete(`/links/${linkId}`, {
25+
headers: {
26+
Authorization: `Bearer ${token}`,
27+
},
28+
});
29+
30+
return res.status(200).json({ message: "링크 삭제 성공" });
31+
} catch (error) {
32+
if (isAxiosError(error) && error.response) {
33+
const status = error.response.status;
34+
const message =
35+
error.response.data?.message || "알 수 없는 오류 발생";
36+
return res.status(status).json({ message });
37+
}
38+
return res.status(500).json({ message: "서버 오류" });
39+
}
40+
// 링크 수정
41+
case "PUT":
42+
if (!linkId) {
43+
return res
44+
.status(400)
45+
.json({ message: "업데이트할 링크 ID가 필요합니다." });
46+
}
47+
48+
const updateData = req.body;
49+
if (!updateData) {
50+
return res
51+
.status(400)
52+
.json({ message: "업데이트할 데이터가 필요합니다." });
53+
}
54+
55+
try {
56+
await axiosInstance.put(`/links/${linkId}`, updateData, {
57+
headers: {
58+
Authorization: `Bearer ${token}`,
59+
},
60+
});
61+
62+
return res.status(200).json({ message: "링크 업데이트 성공" });
63+
} catch (error) {
64+
if (isAxiosError(error) && error.response) {
65+
const status = error.response.status;
66+
const message =
67+
error.response.data?.message || "알 수 없는 오류 발생";
68+
return res.status(status).json({ message });
69+
}
70+
return res.status(500).json({ message: "서버 오류" });
71+
}
72+
73+
default:
74+
return res.status(405).json({ message: "허용되지 않은 접근 방법" });
75+
}
76+
};
77+
78+
export default handler;

pages/favorite/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ interface FavoriteProps {
2222
export const getServerSideProps: GetServerSideProps = async (
2323
context: GetServerSidePropsContext
2424
) => {
25-
const { req } = context;
26-
2725
// 클라이언트의 쿠키 가져오기
26+
const { req } = context;
2827
const cookies = req.headers.cookie || "";
2928

3029
try {
3130
const res = await proxy.get("/api/favorites", {
3231
headers: {
33-
Cookie: cookies, // 쿠키를 그대로 포함시킴
32+
Cookie: cookies,
3433
},
3534
});
3635

0 commit comments

Comments
 (0)