Skip to content
Closed
23 changes: 23 additions & 0 deletions components/Folder/AddFolderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FolderData } from "@/types/folderTypes";
import useModalStore from "@/store/useModalStore";
import useRerenderFolderList from "@/hooks/useRerenderFolderList";

interface AddFolderButtonProps {
setFolderList: React.Dispatch<React.SetStateAction<FolderData[]>>;
}

export const AddFolderButton = ({ setFolderList }: AddFolderButtonProps) => {
const { isOpen, openModal } = useModalStore();

useRerenderFolderList(isOpen, setFolderList);

return (
<button
className="w-[79px] h-[19px] text-purple100"
onClick={() => openModal("AddFolderModal")}
>
폴더 추가 +
</button>
);
};
export default AddFolderButton;
51 changes: 51 additions & 0 deletions components/Folder/FolderActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FolderData } from "@/types/folderTypes";
import Image from "next/image";
import useModalStore from "@/store/useModalStore";
import useRerenderFolderList from "../../hooks/useRerenderFolderList";

interface FolderActionsMenuProps {
setFolderList: React.Dispatch<React.SetStateAction<FolderData[]>>;
}

const FolderActionsMenu = ({ setFolderList }: FolderActionsMenuProps) => {
const { isOpen, openModal } = useModalStore();

const handleModalOpen = (text: string) => {
switch (text) {
case "공유":
openModal("SNSModal");
break;
case "이름 변경":
openModal("EditModal");
break;
case "삭제":
openModal("DeleteFolderModal");
break;
default:
break;
}
};

useRerenderFolderList(isOpen, setFolderList);

return (
<div className="w-[192px] h-[18px] flex justify-between gap-[12px] text-gray400">
{[
{ src: "/icons/share.svg", alt: "공유", text: "공유" },
{ src: "/icons/pen.svg", alt: "이름 변경", text: "이름 변경" },
{ src: "/icons/delete.svg", alt: "삭제", text: "삭제" },
].map(({ src, alt, text }) => (
<button
key={text}
className="flex items-center gap-[4px] text-sm"
onClick={() => handleModalOpen(text)}
>
<Image width={18} height={18} src={src} alt={alt} />
<span>{text}</span>
</button>
))}
</div>
);
};

export default FolderActionsMenu;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const FolderTag = ({ folderList }: FolderListData) => {
const handleSubmit = (id: number | string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, folder: id },
query: id ? { folder: id } : {},
});
};

Expand Down
18 changes: 0 additions & 18 deletions components/Link/ActionButtons.tsx

This file was deleted.

7 changes: 6 additions & 1 deletion components/Link/AddLinkInput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { ChangeEvent, KeyboardEvent, useState } from "react";
import { FolderListData } from "@/types/folderTypes";
import { Modal } from "../modal/modalManager/ModalManager";
import Image from "next/image";
import SubmitButton from "../SubMitButton";
import useModalStore from "@/store/useModalStore";

const AddLinkInput = ({ folderList }: FolderListData) => {
const { isOpen, openModal } = useModalStore();
const [link, setLink] = useState("");

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

const handleClick = () => {
// Addmodal 띄우면서 link 전달
openModal("AddModal", { list: folderList, link: link });
setLink("");
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

잘 적용해주셨군요👍
영상 올려주신거 보니까 모달 한번 클릭하면 isLoading & 버튼 disabled 처리해서 중복으로 추가 방지하도록 해야겠습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

링크 추가 후에도 인풋에 입력값이 남아있어서 여기서 초기값 "" 으로 setLink 해주는게 좋을것같아요!


const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -34,6 +38,7 @@ const AddLinkInput = ({ folderList }: FolderListData) => {
<div onClick={handleClick}>
<SubmitButton className="w-[80px] h-[37px]">추가하기</SubmitButton>
</div>
{isOpen && <Modal />}
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion components/LinkCard.tsx → components/Link/LinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 Dropdown from "../Dropdown";
import useModalStore from "@/store/useModalStore";

interface LinkCardProps {
Expand Down
1 change: 1 addition & 0 deletions components/Search/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const SearchInput = () => {
pathname: router.pathname,
query: { ...router.query, search: value },
});
setValue("");
};

return (
Expand Down
14 changes: 14 additions & 0 deletions components/Search/SearchResultMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface SearchResultMessageProps {
message: string | string[];
}

const SearchResultMessage = ({ message }: SearchResultMessageProps) => {
return (
<div className="text-[32px] text-gray400 mt-[40px]">
<span className="text-black300">&quot;{message}&quot;</span>으로 검색한
결과입니다.
</div>
);
};

export default SearchResultMessage;
19 changes: 19 additions & 0 deletions hooks/useFetchLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from "react";
import { proxy } from "@/lib/api/axiosInstanceApi";
import { LinkData } from "@/types/linkTypes";

// 검색어에 맞는 리스트로 setLinkCardList 해주는 함수
const useFetchLinks = (
search: string | string[] | undefined,
setLinkCardList: (list: LinkData[]) => void
) => {
useEffect(() => {
const fetchLinks = async () => {
const res = await proxy.get("/api/links", { params: { search } });
setLinkCardList(res.data.list);
};
if (search) fetchLinks();
}, [search, setLinkCardList]);
};

export default useFetchLinks;
29 changes: 29 additions & 0 deletions hooks/useRerenderFolderList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useRef } from "react";
import { getFolders } from "@/lib/api/folder";
import { FolderData } from "@/types/folderTypes";

// Folder 관련 드랍다운이 닫혔을 때 화면에 update된 FolderList를 보여주는 커스텀 훅
const useRerenderFolderList = (
isOpen: boolean,
setFolderList: React.Dispatch<React.SetStateAction<FolderData[]>>
) => {
const isFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return; // 최초 로드 시에 불필요한 fetch 요청을 막아줌.
}

const fetchNewFolderList = async () => {
const res = await getFolders();
setFolderList(res);
};

if (!isOpen) {
fetchNewFolderList(); // 드랍다운이 한번 열리고 닫혔을 때 데이터 fetch
}
}, [isOpen, setFolderList]);
};

export default useRerenderFolderList;
4 changes: 2 additions & 2 deletions lib/api/axiosInstanceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import axios from "axios";

const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000",
timeout: 5000,
timeout: 10000,
withCredentials: true,
});

export const proxy = axios.create({
// 배포 이후에는 배포된 URL로 변경해야 함.
baseURL: "http://localhost:3000",
timeout: 5000,
timeout: 10000,
withCredentials: true,
});

Expand Down
10 changes: 10 additions & 0 deletions lib/api/fetchProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { proxy } from "./axiosInstanceApi";

// SSR에서 proxy로 요청 보낼 때 사용하는 로직 ㅜㅊ상화
const fetchProxy = async (endpoint: string, req: any) => {
const headers = req ? { Cookie: req.headers.cookie } : undefined;
const response = await proxy.get(endpoint, { headers });
return response.data;
};

export default fetchProxy;
2 changes: 2 additions & 0 deletions pages/api/links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import axiosInstance from "@/lib/api/axiosInstanceApi";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const cookies = parse(req.headers.cookie || "");
const accessToken = cookies.accessToken;
const { page, search } = req.query;

switch (req.method) {
case "GET":
// 유저의 전체 링크 조회
try {
const response = await axiosInstance.get("/links", {
params: { page, search },
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.status(201).json(response.data);
Expand Down
Binary file removed pages/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file removed pages/fonts/GeistVF.woff
Binary file not shown.
80 changes: 40 additions & 40 deletions pages/link/index.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,68 @@
import { useEffect, useState } from "react";
import { GetServerSidePropsContext } from "next";
import { proxy } from "@/lib/api/axiosInstanceApi";
import { useRouter } from "next/router";
import { LinkData } from "@/types/linkTypes";
import { FolderData } from "@/types/folderTypes";
import { SearchInput } from "../../components/Search/SearchInput";
import { Modal } from "@/components/modal/modalManager/ModalManager";
import { useLinkCardStore } from "@/store/useLinkCardStore";
import { useEffect } from "react";
import Container from "@/components/Layout/Container";
import { SearchInput } from "../../components/Search/SearchInput";
import fetchProxy from "@/lib/api/fetchProxy";
import useModalStore from "@/store/useModalStore";
import useFetchLinks from "@/hooks/useFetchLinks";
import CardsLayout from "@/components/Layout/CardsLayout";
import ActionButtons from "@/components/Link/ActionButtons";
import Container from "@/components/Layout/Container";
import FolderActionsMenu from "@/components/Folder/FolderActionsMenu";
import AddLinkInput from "@/components/Link/AddLinkInput";
import FolderTag from "../../components/FolderTag";
import LinkCard from "../../components/LinkCard";
import useModalStore from "@/store/useModalStore";
import SearchResultMessage from "@/components/Search/SearchResultMessage";
import LinkCard from "@/components/Link/LinkCard";
import AddFolderButton from "@/components/Folder/AddFolderButton";
import FolderTag from "../../components/Folder/FolderTag";

interface LinkPageProps {
linkList: LinkData[];
folderList: FolderData[];
}

// /link 페이지 접속시에 초기렌더링 데이터(전체 폴더, 전체링크리스트)만 fetch해서 client로 전달.
export const getServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { req } = context;

const fetchData = async (endpoint: string) => {
const response = await proxy.get(endpoint, {
headers: {
Cookie: req.headers.cookie,
},
});
return response.data;
};

const [links, folders] = await Promise.all([
fetchData("/api/links"),
fetchData("/api/folders"),
fetchProxy("/api/links", context.req),
fetchProxy("/api/folders", context.req),
]);

return {
props: {
link: links || [],
linkList: links.list || [],
folderList: folders || [],
},
};
};

const LinkPage = ({ linkList, folderList }: LinkPageProps) => {
const LinkPage = ({
linkList: initialLinkList,
folderList: initialFolderList,
}: LinkPageProps) => {
const router = useRouter();
const { search } = router.query;
const { isOpen, openModal } = useModalStore();
const { linkCardList, setLinkCardList } = useLinkCardStore();
const [folderList, setFolderList] = useState(initialFolderList);

useFetchLinks(search, setLinkCardList);

// 클라이언트에서 초기 목록을 설정
useEffect(() => {
setLinkCardList(linkList);
}, [linkList, setLinkCardList]);

// EditLink 호출
const openEdit = (link: string, linkId: number) => {
openModal("EditLink", { link, linkId: linkId ?? null });
};
setLinkCardList(initialLinkList);
}, [initialLinkList, setLinkCardList]);

// DeleteLinkModal 호출
const openDelete = (link: string, linkId: number) => {
openModal("DeleteLinkModal", { link, linkId: linkId ?? null });
const handleModalOpen = (
type: "EditLink" | "DeleteLinkModal",
link: string,
linkId: number
) => {
openModal(type, { link, linkId });
};

return (
Expand All @@ -74,22 +73,23 @@ const LinkPage = ({ linkList, folderList }: LinkPageProps) => {
<main className="mt-[40px]">
<Container>
<SearchInput />
{search && <SearchResultMessage message={search} />}
<div className="flex justify-between mt-[40px]">
{folderList && <FolderTag folderList={folderList} />}
<button className="w-[79px] h-[19px] text-purple100">
폴더 추가 +
</button>
<AddFolderButton setFolderList={setFolderList} />
</div>
<div className="flex justify-between items-center mt-[24px]">
<div className="flex justify-between items-center my-[24px]">
<h1 className="text-2xl ">유용한 글</h1>
<ActionButtons />
<FolderActionsMenu setFolderList={setFolderList} />
</div>
<CardsLayout>
{linkCardList.map((link) => (
<LinkCard
key={link.id}
openEdit={() => openEdit(link.url, link.id)}
openDelete={() => openDelete(link.url, link.id)}
openEdit={() => handleModalOpen("EditLink", link.url, link.id)}
openDelete={() =>
handleModalOpen("DeleteLinkModal", link.url, link.id)
}
info={link}
/>
))}
Expand Down
Loading