diff --git a/components/Folder/AddFolderButton.tsx b/components/Folder/AddFolderButton.tsx new file mode 100644 index 0000000..5f48fb4 --- /dev/null +++ b/components/Folder/AddFolderButton.tsx @@ -0,0 +1,23 @@ +import { FolderData } from "@/types/folderTypes"; +import useModalStore from "@/store/useModalStore"; +import useRerenderFolderList from "@/hooks/useRerenderFolderList"; + +interface AddFolderButtonProps { + setFolderList: React.Dispatch>; +} + +export const AddFolderButton = ({ setFolderList }: AddFolderButtonProps) => { + const { isOpen, openModal } = useModalStore(); + + useRerenderFolderList(isOpen, setFolderList); + + return ( + + ); +}; +export default AddFolderButton; diff --git a/components/Folder/FolderActionsMenu.tsx b/components/Folder/FolderActionsMenu.tsx new file mode 100644 index 0000000..c54dcfd --- /dev/null +++ b/components/Folder/FolderActionsMenu.tsx @@ -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>; +} + +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 ( +
+ {[ + { src: "/icons/share.svg", alt: "공유", text: "공유" }, + { src: "/icons/pen.svg", alt: "이름 변경", text: "이름 변경" }, + { src: "/icons/delete.svg", alt: "삭제", text: "삭제" }, + ].map(({ src, alt, text }) => ( + + ))} +
+ ); +}; + +export default FolderActionsMenu; diff --git a/components/FolderTag.tsx b/components/Folder/FolderTag.tsx similarity index 95% rename from components/FolderTag.tsx rename to components/Folder/FolderTag.tsx index 33e91b4..b4768c2 100644 --- a/components/FolderTag.tsx +++ b/components/Folder/FolderTag.tsx @@ -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 } : {}, }); }; diff --git a/components/Link/ActionButtons.tsx b/components/Link/ActionButtons.tsx deleted file mode 100644 index b498677..0000000 --- a/components/Link/ActionButtons.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Image from "next/image"; - -const ActionButtons = () => ( -
- {[ - { src: "/icons/share.svg", alt: "공유", text: "공유" }, - { src: "/icons/pen.svg", alt: "이름 변경", text: "이름 변경" }, - { src: "/icons/delete.svg", alt: "삭제", text: "삭제" }, - ].map(({ src, alt, text }) => ( - - ))} -
-); - -export default ActionButtons; diff --git a/components/Link/AddLinkInput.tsx b/components/Link/AddLinkInput.tsx index add6904..d70a003 100644 --- a/components/Link/AddLinkInput.tsx +++ b/components/Link/AddLinkInput.tsx @@ -1,9 +1,12 @@ 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) => { @@ -11,7 +14,8 @@ const AddLinkInput = ({ folderList }: FolderListData) => { }; const handleClick = () => { - // Addmodal 띄우면서 link 전달 + openModal("AddModal", { list: folderList, link: link }); + setLink(""); }; const handleKeyDown = (e: KeyboardEvent) => { @@ -34,6 +38,7 @@ const AddLinkInput = ({ folderList }: FolderListData) => {
추가하기
+ {isOpen && } ); }; diff --git a/components/LinkCard.tsx b/components/Link/LinkCard.tsx similarity index 98% rename from components/LinkCard.tsx rename to components/Link/LinkCard.tsx index fccbc83..4081abe 100644 --- a/components/LinkCard.tsx +++ b/components/Link/LinkCard.tsx @@ -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 { diff --git a/components/Search/SearchInput.tsx b/components/Search/SearchInput.tsx index 92d2e13..b73fa1c 100644 --- a/components/Search/SearchInput.tsx +++ b/components/Search/SearchInput.tsx @@ -16,6 +16,7 @@ export const SearchInput = () => { pathname: router.pathname, query: { ...router.query, search: value }, }); + setValue(""); }; return ( diff --git a/components/Search/SearchResultMessage.tsx b/components/Search/SearchResultMessage.tsx new file mode 100644 index 0000000..4ee0f8e --- /dev/null +++ b/components/Search/SearchResultMessage.tsx @@ -0,0 +1,14 @@ +interface SearchResultMessageProps { + message: string | string[]; +} + +const SearchResultMessage = ({ message }: SearchResultMessageProps) => { + return ( +
+ "{message}"으로 검색한 + 결과입니다. +
+ ); +}; + +export default SearchResultMessage; diff --git a/hooks/useFetchLinks.tsx b/hooks/useFetchLinks.tsx new file mode 100644 index 0000000..f7a7efd --- /dev/null +++ b/hooks/useFetchLinks.tsx @@ -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; diff --git a/hooks/useRerenderFolderList.tsx b/hooks/useRerenderFolderList.tsx new file mode 100644 index 0000000..080aabf --- /dev/null +++ b/hooks/useRerenderFolderList.tsx @@ -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> +) => { + 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; diff --git a/lib/api/axiosInstanceApi.ts b/lib/api/axiosInstanceApi.ts index 3b15de6..a037316 100644 --- a/lib/api/axiosInstanceApi.ts +++ b/lib/api/axiosInstanceApi.ts @@ -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, }); diff --git a/lib/api/fetchProxy.ts b/lib/api/fetchProxy.ts new file mode 100644 index 0000000..07ad392 --- /dev/null +++ b/lib/api/fetchProxy.ts @@ -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; diff --git a/pages/api/links/index.ts b/pages/api/links/index.ts index 0b8f68f..32ae519 100644 --- a/pages/api/links/index.ts +++ b/pages/api/links/index.ts @@ -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); diff --git a/pages/fonts/GeistMonoVF.woff b/pages/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185..0000000 Binary files a/pages/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/pages/fonts/GeistVF.woff b/pages/fonts/GeistVF.woff deleted file mode 100644 index 1b62daa..0000000 Binary files a/pages/fonts/GeistVF.woff and /dev/null differ diff --git a/pages/link/index.tsx b/pages/link/index.tsx index 7a0bbf7..ec2d136 100644 --- a/pages/link/index.tsx +++ b/pages/link/index.tsx @@ -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 ( @@ -74,22 +73,23 @@ const LinkPage = ({ linkList, folderList }: LinkPageProps) => {
+ {search && }
{folderList && } - +
-
+

유용한 글

- +
{linkCardList.map((link) => ( 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} /> ))} diff --git a/store/useLinkCardStore.tsx b/store/useLinkCardStore.tsx index 9445ed4..68e8931 100644 --- a/store/useLinkCardStore.tsx +++ b/store/useLinkCardStore.tsx @@ -1,24 +1,15 @@ import { deleteLinkURL, getLinks, putLinkURL } from "@/lib/api/link"; import { create } from "zustand"; - -interface LinkCardDataType { - id: number; - favorite: boolean; - url: string; - title: string; - imageSource: string; - description: string; - createdAt: string; -} +import { LinkData } from "@/types/linkTypes"; interface UpdateLinkBody { url: string; } interface LinkCardStore { - linkCardList: LinkCardDataType[]; + linkCardList: LinkData[]; totalCount: number; - setLinkCardList: (list: LinkCardDataType[]) => void; + setLinkCardList: (list: LinkData[]) => void; updateLink: (linkId: number, body: UpdateLinkBody) => Promise; deleteLink: (linkId: number) => Promise; } @@ -27,7 +18,7 @@ export const useLinkCardStore = create((set) => ({ linkCardList: [], totalCount: 0, - setLinkCardList: (list: LinkCardDataType[]) => { + setLinkCardList: (list: LinkData[]) => { set({ linkCardList: list, totalCount: list.length }); },