Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";

interface DropdownProps {
onEdit?: () => void;
openDelete?: () => void;
}

const Dropdown = ({ onEdit, openDelete }: DropdownProps) => {
const buttonStyle =
"block w-full py-2 text-sm hover:bg-gray200 hover:text-purple100";

return (
<div className="absolute top-[17px] right-0 flex flex-col gap-[2px] min-w-[100px] bg-white shadow-lg rounded">
<button className={buttonStyle} onClick={onEdit}>
수정하기
</button>
<button className={buttonStyle} onClick={openDelete}>
삭제하기
</button>
</div>
);
};

export default Dropdown;
84 changes: 47 additions & 37 deletions components/LinkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import timeAgo from "@/util/timAgo";
import Image from "next/image";
import Dropdown from "./Dropdown";
import useModalStore from "@/store/useModalStore";

interface LinkCardProps {
info: {
Expand All @@ -12,16 +15,29 @@ interface LinkCardProps {
url: string;
createdAt: string;
};
isFavoritePage?: boolean;
onEdit?: () => void;
openDelete?: () => void;
}

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

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

const router = useRouter();
const isFavoritePage = router.pathname === "/favorite";

// 모달이 열릴 때 드롭다운 닫기
useEffect(() => {
if (isModalOpen) setIsDropdownOpen(false);
}, [isModalOpen]);

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

return (
<div className="w-[340px] h-[344px] rounded-[12px] shadow-lg overflow-hidden cursor-pointer hover:scale-105 hover:duration-300">
<section className="relative w-full h-[60%]">
Expand All @@ -31,47 +47,41 @@ const LinkCard = ({ isFavoritePage, info }: LinkCardProps) => {
alt="링크 미리보기"
fill
/>
{/* isFavoritePage가 false일 때만 즐겨찾기 버튼 렌더링 */}
{!isFavoritePage &&
(isSubscribed ? (
<div
onClick={() => setIsSubscribed(!isSubscribed)}
className="absolute top-[15px] right-[15px] z-1"
>
<Image
src="/icons/star-fill.svg"
width={32}
height={32}
alt="subscripe button"
/>
</div>
) : (
<div
onClick={() => setIsSubscribed(!isSubscribed)}
className="absolute top-[15px] right-[15px] z-1"
>
<Image
src="/icons/star-empty.svg"
width={32}
height={32}
alt="subscripe button"
/>
</div>
))}
{/* isFavoritePage일 때만 즐겨찾기 버튼 렌더링 */}
{!isFavoritePage && (
<div
onClick={() => setIsSubscribed(!isSubscribed)}
className="absolute top-[15px] right-[15px] z-1"
>
<Image
src={
isSubscribed ? "/icons/star-fill.svg" : "/icons/star-empty.svg"
}
width={32}
height={32}
alt="subscribe button"
/>
</div>
)}
</section>

<section className="w-full h-[40%] flex flex-col justify-between gap-[10px] pt-[15px] px-[20px] pb-[10px]">
<div className="flex justify-between">
<span className="text-sm text-gray-400">
{createdTime || "1일 전"}
</span>
{/* isFavoritePage가 false일 때만 케밥 버튼 렌더링 */}
{/* isFavoritePage일 때만 케밥 버튼 렌더링 */}
{!isFavoritePage && (
<div
className="relative w-[21px] h-[17px]"
onClick={(state) => setIsOpen(!state)}
>
<Image src="/icons/kebab.svg" alt="kebab button" fill />
<div className="relative">
<button
className="relative w-[21px] h-[17px]"
onClick={toggleDropdown}
>
<Image src="/icons/kebab.svg" alt="kebab button" fill />
</button>
{isDropdownOpen && (
<Dropdown onEdit={onEdit} openDelete={openDelete} />
)}
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/modal/AddFolderModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeEvent, useState } from "react";
import { postFolders } from "@/lib/api/folder";
import ModalContainer from "./modalComponents/ModalContainer";
import ModalInput from "./modalComponents/ModalInput";
import { postFolders } from "@/lib/api/folder";
import useModalStore from "@/store/useModalStore";
import SubmitButton from "../SubMitButton";

Expand Down
26 changes: 23 additions & 3 deletions components/modal/DeleteLinkModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import useModalStore from "@/store/useModalStore";
import SubmitButton from "../SubMitButton";
import ModalContainer from "./modalComponents/ModalContainer";
import { useLinkCardStore } from "@/store/useLinkCardStore";

const DeleteLinkModal = ({
link,
linkId,
}: {
link: string;
linkId: number;
}) => {
const { closeModal } = useModalStore();
const { deleteLink } = useLinkCardStore();

const handleDelete = async () => {
try {
await deleteLink(linkId);
closeModal();
} catch (error) {
console.error("Failed to delete the link:", error);
}
};

const DeleteLinkModal = ({ link }: { link: string }) => {
return (
<ModalContainer title="링크 삭제" subtitle={link}>
<SubmitButton
type="button"
// onClick={handleSubmit}
onClick={handleDelete}
width="w-full"
height="h-[51px] "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모달 파일마다 공백이 있었더라구요 😅👍

height="h-[51px]"
color="negative"
>
삭제하기
Expand Down
54 changes: 54 additions & 0 deletions components/modal/EditLink.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안그래도 요걸 제가 안만들어놨었는데 추가해주셨군요!👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ChangeEvent, useState } from "react";
import { useLinkCardStore } from "@/store/useLinkCardStore";
import ModalContainer from "./modalComponents/ModalContainer";
import ModalInput from "./modalComponents/ModalInput";
import useModalStore from "@/store/useModalStore";
import SubmitButton from "../SubMitButton";

const EditLink = ({
folderName,
link,
linkId,
}: {
folderName: string;
link: string;
linkId: number;
}) => {
const [value, setValue] = useState("");
const { closeModal } = useModalStore();
const { updateLink } = useLinkCardStore();

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};

const handleSubmit = async () => {
const body = {
url: value,
};
if (value !== "") {
await updateLink(linkId, body);
}
closeModal();
};
return (
<ModalContainer title="링크 주소 변경">
<ModalInput
placeholder={link}
name={folderName}
value={value}
onChange={handleChange}
/>
<SubmitButton
type="button"
onClick={handleSubmit}
width="w-full"
height="h-[51px] "
color="positive"
>
변경하기
</SubmitButton>
</ModalContainer>
);
};
export default EditLink;
17 changes: 16 additions & 1 deletion components/modal/modalManager/ModalManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AddFolderModal from "../AddFolderModal";
import DeleteLinkModal from "../DeleteLinkModal";
import EditModal from "../EditModal";
import SNSModal from "../SNSModal";
import EditLink from "../EditLink";

export const ModalType = {
AddFolderModal: "AddFolderModal",
Expand All @@ -13,6 +14,7 @@ export const ModalType = {
DeleteLinkModal: "DeleteLinkModal",
EditModal: "EditModal",
SNSModal: "SNSModal",
EditLink: "EditLink",
} as const;

export type ModalKeysType = keyof typeof ModalType;
Expand Down Expand Up @@ -42,10 +44,23 @@ export const Modal = () => {
case "DeleteFolderModal":
return <DeleteFolderModal folderName={props.folderName || "폴더이름"} />;
case "DeleteLinkModal":
return <DeleteLinkModal link={props.link || "링크"} />;
return (
<DeleteLinkModal
link={props.link || "링크"}
linkId={Number(props.linkId)}
/>
);
case "EditModal":
return <EditModal folderName={props.folderName || "폴더이름"} />;
case "SNSModal":
return <SNSModal folderName={props.folderName || "폴더이름"} />;
case "EditLink":
return (
<EditLink
folderName={props.folderName || "폴더이름"}
link={props.link || "링크"}
linkId={Number(props.linkId)}
/>
);
}
};
78 changes: 78 additions & 0 deletions pages/api/links/[linkId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 "DELETE":
if (!linkId) {
return res
.status(400)
.json({ message: "삭제할 링크 ID가 필요합니다." });
}

try {
await axiosInstance.delete(`/links/${linkId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});

return res.status(200).json({ message: "링크 삭제 성공" });
} 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: "서버 오류" });
}
// 링크 수정
case "PUT":
if (!linkId) {
return res
.status(400)
.json({ message: "업데이트할 링크 ID가 필요합니다." });
}

const updateData = req.body;
if (!updateData) {
return res
.status(400)
.json({ message: "업데이트할 데이터가 필요합니다." });
}

try {
await axiosInstance.put(`/links/${linkId}`, updateData, {
headers: {
Authorization: `Bearer ${token}`,
},
});

return res.status(200).json({ message: "링크 업데이트 성공" });
} 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;
5 changes: 2 additions & 3 deletions pages/favorite/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ interface FavoriteProps {
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { req } = context;

// 클라이언트의 쿠키 가져오기
const { req } = context;
const cookies = req.headers.cookie || "";

try {
const res = await proxy.get("/api/favorites", {
headers: {
Cookie: cookies, // 쿠키를 그대로 포함시킴
Cookie: cookies,
},
});

Expand Down
Loading
Loading