Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
17 changes: 17 additions & 0 deletions components/Favorite/EmptyFavoriteList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";

const EmptyFavoriteList = () => {
return (
<div className="flex flex-col justify-center items-center h-full p-10 bg-gray100 text-center text-gray600">
<div className="text-2xl md:text-3xl font-semibold text-gray600">
<span className="block mb-4">⭐️</span>
즐겨찾기 항목이 없습니다.
</div>
<div className="text-sm text-purple100 mt-2">
저장한 즐겨찾기 항목을 추가해보세요.
</div>
</div>
);
};

export default EmptyFavoriteList;
2 changes: 1 addition & 1 deletion components/Link/LinkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/router";
import { useLinkCardStore } from "@/store/useLinkCardStore";
import { ensureAbsoluteUrl } from "@/lib/utils";
import { ensureAbsoluteUrl } from "@/util/ensureAbsoluteUrl";
import timeAgo from "@/util/timeAgo";
import Image from "next/image";
import Dropdown from "../Dropdown";
Expand Down
2 changes: 1 addition & 1 deletion components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Pagination: React.FC<PaginationProps> = ({ totalCount }) => {

const { page, pageSize } = router.query;
const currentPage = Number(page) || 1;
const currentPageSize = Number(pageSize) || 6;
const currentPageSize = Number(pageSize) || 9;
const totalPages = Math.ceil(totalCount / currentPageSize);

const [maxPagesToShow, setMaxPagesToShow] = useState(2);
Expand Down
39 changes: 26 additions & 13 deletions hooks/useFetchLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { useEffect } from "react";
import { useState, useEffect } from "react";
import { proxy } from "@/lib/api/axiosInstanceApi";
import { LinkData } from "@/types/linkTypes";
import { ParsedUrlQuery } from "querystring";
import useViewport from "./useViewport";

// 링크페이지의 query가 바뀌면 그에 맞는 링크들을 보여주는 훅
// 링크 페이지의 query가 바뀌면 그에 맞는 링크들을 보여주는 훅
const useFetchLinks = (
setLinkCardList: React.Dispatch<React.SetStateAction<LinkData[]>>,
setTotalCount?: React.Dispatch<React.SetStateAction<number>>,
query?: ParsedUrlQuery,
pathname?: string
) => {
const { isTablet } = useViewport();
const [loading, setLoading] = useState<boolean>(false); // 로딩 상태 관리

useEffect(() => {
const fetchLinks = async () => {
if (isTablet === undefined) return; // isTablet이 정의되지 않았으면 API 호출을 막음

setLoading(true); // API 호출 시작 시 로딩 상태 true

// 경로에 따라 API 엔드포인트 분기
let endpoint =
pathname === "/favorite"
Expand All @@ -23,21 +28,29 @@ const useFetchLinks = (
? `/api/folders/${query.folder}/links`
: "/api/links";

const res = await proxy.get(endpoint, {
params: {
page: query?.page,
pageSize: isTablet ? 6 : 10,
search: query?.search,
},
});
console.log("query가 바뀌었을 때 다시 받아온 리스트:", res.data.list);
setLinkCardList(res.data.list);
{
setTotalCount && setTotalCount(res.data.totalCount);
try {
const res = await proxy.get(endpoint, {
params: {
page: query?.page,
pageSize: isTablet ? 6 : 9,
search: query?.search,
},
});
setLinkCardList(res.data.list);
if (setTotalCount) {
setTotalCount(res.data.totalCount);
}
} catch (error) {
console.error("Error fetching links:", error);
} finally {
setLoading(false); // API 호출 종료 후 로딩 상태 false
}
};

if (query) fetchLinks();
}, [setLinkCardList, query, isTablet]);

return loading; // 로딩 상태 반환
};

export default useFetchLinks;
14 changes: 11 additions & 3 deletions hooks/useViewport.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react";
import debounce from "lodash.debounce";

const breakpoints = {
PC: { min: 1200 },
Expand All @@ -10,14 +11,21 @@ const breakpoints = {
function useViewport(initialWidth = 0) {
const [width, setWidth] = useState(initialWidth);

const handleResize = () => {
// debounce를 사용하여 resize 이벤트 핸들러 생성
const handleResize = debounce(() => {
setWidth(window.innerWidth);
};
}, 200); // 200ms 지연

useEffect(() => {
handleResize();

window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);

return () => {
// 컴포넌트 언마운트 시 이벤트 리스너 제거 및 디바운스 정리
window.removeEventListener("resize", handleResize);
handleResize.cancel(); // debounce 취소
};
}, []);

const isPC = width >= breakpoints.PC.min;
Expand Down
6 changes: 0 additions & 6 deletions lib/utils.ts
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
@@ -1,9 +1,3 @@
// tailwind 동적 스타일을 위한 함수
export const bindCls = (...cls: string[]) => {
return cls.join(" ");
};

// 데이터나 API에서 이미지 URL이 //로 오는 경우 자동으로 프로토콜 추가
export const ensureAbsoluteUrl = (url: string) => {
return url.startsWith("//") ? `https:${url}` : url;
};
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"axios": "^1.7.7",
"cookie": "^1.0.1",
"jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"next": "15.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -21,6 +22,7 @@
"zustand": "^5.0.1"
},
"devDependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
2 changes: 1 addition & 1 deletion pages/api/favorites/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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, pageSize = 6 } = req.query;
const { page, pageSize } = req.query;

if (!accessToken) {
return res.status(401).json({ message: "인증 오류: 토큰이 없습니다." });
Expand Down
2 changes: 1 addition & 1 deletion pages/api/links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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, pageSize = 6, search } = req.query;
const { page, pageSize, search } = req.query;

switch (req.method) {
case "GET":
Expand Down
72 changes: 48 additions & 24 deletions pages/favorite/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import useFetchLinks from "@/hooks/useFetchLinks";
import { useRouter } from "next/router";
import { useState } from "react";
import { parse } from "cookie";
import LoadingSpinner from "@/components/LoadingSpinner";
import EmptyFavoriteList from "@/components/Favorite/EmptyFavoriteList";

interface FavoriteDataType {
id: number;
Expand All @@ -31,11 +33,20 @@ export const getServerSideProps: GetServerSideProps = async (
const { req } = context;
const cookies = parse(req.headers.cookie || "");
const accessToken = cookies.accessToken;

try {
const res = await axiosInstance.get("/favorites?page=1&pageSize=10", {
if (!accessToken) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}

const res = await axiosInstance.get("/favorites", {
headers: { Authorization: `Bearer ${accessToken}` },
});

const { list, totalCount } = res.data || { list: [], totalCount: 0 };
return { props: { favoriteList: list, totalCount } };
} catch (error) {
Expand All @@ -44,42 +55,55 @@ export const getServerSideProps: GetServerSideProps = async (
}
};

const FavoritePage = ({ favoriteList, totalCount }: FavoriteProps) => {
const FavoritePage = ({
favoriteList,
totalCount: initialTotalCount,
}: FavoriteProps) => {
const router = useRouter();

const [linkCardList, setLinkCardList] =
useState<FavoriteDataType[]>(favoriteList);
const [totalCount, setTotalCount] = useState(initialTotalCount);

const loading = useFetchLinks(setLinkCardList, setTotalCount, router.query);

// 마이링크 페이지로 돌아감
const returnButton = () => {
router.push(`/link`);
};

useFetchLinks(setLinkCardList);
return (
<>
<div className="page-title pt-[10px] md:pt-5 pb-10 md:pb-[60px] bg-gray100 text-center">
<div className="flex justify-center items-center sm:h-[117px] h-[219px] sm:mb-5 mb-10 bg-gray100 text-center">
<h2 className="text-[32px] md:text-[40px] font-semibold">
⭐️ 즐겨찾기
</h2>
</div>
<Container>
<CardsLayout>
{linkCardList.length > 0
? linkCardList.map((favorite) => (
<LinkCard key={favorite.id} info={favorite} />
))
: null}
</CardsLayout>
<div className="flex justify-end">
<button onClick={returnButton} className="mb-5 text-purple100">
👈 마이링크로 돌아가기
</button>
</div>

{/* 즐겨찾기 항목이 없을 때 보여줄 메시지 (공통 컴포넌트로 사용할 건지 논의 필요) */}
{favoriteList.length === 0 && (
<div className="flex flex-col justify-center items-center h-full p-10 bg-gray100 text-center text-gray600">
<div className="text-2xl md:text-3xl font-semibold text-gray600">
<span className="block mb-4">⭐️</span>
즐겨찾기 항목이 없습니다.
</div>
<div className="text-sm text-purple100 mt-2">
저장한 즐겨찾기 항목을 추가해보세요.
</div>
{/* 로딩 중일 때 */}
{loading ? (
<div className="text-center">
<LoadingSpinner />
</div>
) : linkCardList.length > 0 ? (
<>
<CardsLayout>
{linkCardList.length > 0
? linkCardList.map((favorite) => (
<LinkCard key={favorite.id} info={favorite} />
))
: null}
</CardsLayout>
<Pagination totalCount={totalCount} />
</>
) : (
<EmptyFavoriteList />
)}
<Pagination totalCount={totalCount} />
</Container>
</>
);
Expand Down
18 changes: 14 additions & 4 deletions pages/link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import LinkCard from "@/components/Link/LinkCard";
import RenderEmptyLinkMessage from "@/components/Link/RenderEmptyLinkMessage";
import useFetchLinks from "@/hooks/useFetchLinks";
import useViewport from "@/hooks/useViewport";
import LoadingSpinner from "@/components/LoadingSpinner";

interface LinkPageProps {
linkList: LinkData[];
Expand Down Expand Up @@ -72,9 +73,12 @@ const LinkPage = ({
const [totalCount, setTotalCount] = useState(initialTotalCount);

// 링크페이지의 query가 바뀌면 새로운 리스트로 업데이트 해주는 훅
useFetchLinks(setLinkCardList, setTotalCount, router.query, router.pathname);

console.log(linkCardList);
const loading = useFetchLinks(
setLinkCardList,
setTotalCount,
router.query,
router.pathname
);

return (
<>
Expand All @@ -99,7 +103,13 @@ const LinkPage = ({
/>
)}
</div>
{linkCardList ? (

{/* 로딩 중일 때 */}
{loading ? (
<div className="text-center">
<LoadingSpinner />
</div>
) : linkCardList.length > 0 ? (
<>
<CardsLayout>
{linkCardList.map((link) => (
Expand Down
4 changes: 4 additions & 0 deletions util/ensureAbsoluteUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// 데이터나 API에서 이미지 URL이 //로 오는 경우 자동으로 프로토콜 추가
export const ensureAbsoluteUrl = (url: string) => {
return url.startsWith("//") ? `https:${url}` : url;
};
Loading