Skip to content

Commit 8f0ffe8

Browse files
authored
Merge pull request #102 from hyeonjiroh/feat/#22/sidebar-pagenation
feat: #22/사이드 바 페이지네이션 구현
2 parents a8b39dc + 3cce981 commit 8f0ffe8

File tree

8 files changed

+122
-41
lines changed

8 files changed

+122
-41
lines changed

src/app/(after-login)/dashboard/[dashboardid]/_components/Column.tsx

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { useEffect, useState, useRef } from "react";
4+
import { useIntersection } from "@/lib/hooks/useIntersection";
45
import { DashboardColumn, TaskCardList } from "@/lib/types";
56
import { fetchTaskCardList } from "@/lib/apis/cardsApi";
67
import { TOKEN_1 } from "@/lib/constants/tokens";
@@ -50,26 +51,11 @@ export default function Column({ id, title }: DashboardColumn) {
5051
handleLoad();
5152
}, []);
5253

53-
useEffect(() => {
54-
if (isLast) return;
55-
56-
const observer = new IntersectionObserver(
57-
(entries) => {
58-
if (entries[0].isIntersecting) {
59-
handleLoad();
60-
}
61-
},
62-
{ threshold: 0.5 }
63-
);
64-
65-
const current = observerRef.current;
66-
if (current) observer.observe(current);
67-
68-
return () => {
69-
if (current) observer.unobserve(current);
70-
observer.disconnect();
71-
};
72-
}, [cursorId, isLoading, isLast]);
54+
useIntersection({
55+
target: observerRef,
56+
onIntersect: handleLoad,
57+
disabled: isLast,
58+
});
7359

7460
return (
7561
<div className="h-full py-4 border-b border-gray-300 tablet:px-5 tablet:pt-[22px] tablet:pb-5 pc:border-b-0 pc:border-r">

src/app/(after-login)/mydashboard/layout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import { TOKEN_1 } from "@/lib/constants/tokens";
44
import DashboardMenu from "@/components/layout/navbar/DashboardMenu";
55
import UserMenu from "@/components/layout/navbar/UserMenu";
66

7+
const PAGE_SIZE = 15;
8+
79
export default async function Layout({
810
children,
911
}: {
1012
children: React.ReactNode;
1113
}) {
12-
// 가장 첫 번째 페이지 리스트 불러오도록 나중에 수정
13-
const { dashboards } = await fetchDashboardList(TOKEN_1);
14+
const { dashboards } = await fetchDashboardList({
15+
token: TOKEN_1,
16+
page: 1,
17+
size: PAGE_SIZE,
18+
});
1419

1520
const firstDashboardId = dashboards[0]?.id;
1621

src/app/(after-login)/mypage/layout.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import { TOKEN_1 } from "@/lib/constants/tokens";
44
import DashboardMenu from "@/components/layout/navbar/DashboardMenu";
55
import UserMenu from "@/components/layout/navbar/UserMenu";
66

7+
const PAGE_SIZE = 15;
8+
79
export default async function Layout({
810
children,
911
}: {
1012
children: React.ReactNode;
1113
}) {
12-
// 가장 첫 번째 페이지 리스트 불러오도록 나중에 수정
13-
const { dashboards } = await fetchDashboardList(TOKEN_1);
14+
const { dashboards } = await fetchDashboardList({
15+
token: TOKEN_1,
16+
page: 1,
17+
size: PAGE_SIZE,
18+
});
1419

1520
const firstDashboardId = dashboards[0]?.id;
1621

src/components/layout/sidebar/SideMenuItem.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use client";
2-
31
import { useIsMobile } from "@/lib/hooks/useCheckViewport";
42
import { useDashboardStore } from "@/lib/store/useDashboardStore";
53
import { useRouter } from "next/navigation";
@@ -26,7 +24,7 @@ export default function SideMenuItem({
2624
onClick={() => {
2725
router.push(`/dashboard/${id}`);
2826
}}
29-
className="flex items-center p-4 rounded hover:bg-violet-8 tablet:px-[10px] tablet:py-2 pc:p-3"
27+
className="flex items-center w-full p-4 rounded hover:bg-violet-8 tablet:px-[10px] tablet:py-2 pc:p-3"
3028
style={isCurrent ? { background: "#f1effd" } : {}}
3129
disabled={isCurrent}
3230
>

src/components/layout/sidebar/SideMenuList.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,62 @@
1+
"use client";
2+
3+
import { useEffect, useState, useRef } from "react";
4+
import { useIntersection } from "@/lib/hooks/useIntersection";
15
import { DashboardList } from "@/lib/types";
26
import { fetchDashboardList } from "@/lib/apis/dashboardsApi";
37
import { TOKEN_1 } from "@/lib/constants/tokens";
48
import SideMenuItem from "./SideMenuItem";
59

6-
export default async function SideMenuList() {
7-
const { dashboards, totalCount, cursorId } =
8-
await fetchDashboardList(TOKEN_1);
9-
const items: DashboardList[] = dashboards;
10+
const PAGE_SIZE = 15;
11+
12+
export default function SideMenuList() {
13+
const [items, setItems] = useState<DashboardList[]>([]);
14+
const [page, setPage] = useState(1);
15+
const [isLoading, setIsLoading] = useState(false);
16+
const [isLast, setIsLast] = useState(false);
17+
const observerRef = useRef<HTMLDivElement | null>(null);
18+
19+
const handleLoad = async () => {
20+
if (isLoading || isLast) return;
21+
setIsLoading(true);
22+
23+
try {
24+
const { dashboards: newDashboards } = await fetchDashboardList({
25+
token: TOKEN_1,
26+
size: PAGE_SIZE,
27+
page,
28+
});
29+
30+
if (newDashboards.length === 0) {
31+
setIsLast(true);
32+
} else {
33+
setItems((prev) => [...prev, ...newDashboards]);
34+
setPage((prev) => prev + 1);
35+
}
36+
} finally {
37+
setIsLoading(false);
38+
}
39+
};
40+
41+
useEffect(() => {
42+
handleLoad();
43+
}, []);
1044

11-
// 해당 값들 사용하게 되면 지울 테스트 코드들
12-
console.log(totalCount);
13-
console.log(cursorId);
45+
useIntersection({
46+
target: observerRef,
47+
onIntersect: handleLoad,
48+
disabled: isLast,
49+
});
1450

1551
return (
16-
<div className="flex flex-col gap-[14px] tablet:gap-[2px] pc:gap-2">
17-
{items.map((item) => (
18-
<SideMenuItem key={item.id} {...item} />
52+
<div className="flex flex-col gap-[14px] flex-grow min-h-0 overflow-y-auto whitespace-nowrap scrollbar-hide tablet:gap-[2px] pc:gap-2">
53+
{items.map((item, index) => (
54+
<div
55+
key={item.id}
56+
ref={index === items.length - 1 ? observerRef : null}
57+
>
58+
<SideMenuItem {...item} />
59+
</div>
1960
))}
2061
</div>
2162
);

src/components/layout/sidebar/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import SideMenuList from "./SideMenuList";
55

66
export default function Sidebar() {
77
return (
8-
<div className="flex flex-col gap-[39px] tablet:gap-[57px] pc:gap-14">
8+
<div className="flex flex-col gap-[39px] h-full tablet:gap-[57px] pc:gap-14">
99
<div className="flex justify-center items-center tablet:justify-start">
1010
<LogoButton variant={"purple"} />
1111
</div>
12-
<div className="flex flex-col items-center gap-[22px] tablet:gap-[15px] tablet:items-stretch pc:gap-[16px]">
12+
<div className="flex flex-col items-center gap-[22px] flex-grow min-h-0 tablet:gap-[15px] tablet:items-stretch pc:gap-[16px]">
1313
<div className="flex justify-between">
1414
<SideMenuHeader />
1515
<AddButton />

src/lib/apis/dashboardsApi.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { BASE_URL } from "@/lib/constants/urls";
22

3-
export async function fetchDashboardList(token: string) {
3+
export async function fetchDashboardList({
4+
token,
5+
page,
6+
size,
7+
}: {
8+
token: string;
9+
page: number;
10+
size: number;
11+
}) {
412
const res = await fetch(
5-
`${BASE_URL}/dashboards?navigationMethod=pagination&page=1&size=10`,
13+
`${BASE_URL}/dashboards?navigationMethod=pagination&page=${page}&size=${size}`,
614
{
715
headers: {
816
Accept: "application/json",

src/lib/hooks/useIntersection.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
interface UseIntersectionProps {
6+
target: React.RefObject<Element>;
7+
onIntersect: () => void;
8+
disabled?: boolean;
9+
threshold?: number;
10+
}
11+
12+
export function useIntersection({
13+
target,
14+
onIntersect,
15+
disabled,
16+
threshold = 0.5,
17+
}: UseIntersectionProps) {
18+
useEffect(() => {
19+
if (disabled) return;
20+
21+
const observer = new IntersectionObserver(
22+
(entries) => {
23+
if (entries[0].isIntersecting) {
24+
onIntersect();
25+
}
26+
},
27+
{ threshold: threshold }
28+
);
29+
30+
const current = target.current;
31+
if (current) observer.observe(current);
32+
33+
return () => {
34+
if (current) observer.unobserve(current);
35+
observer.disconnect();
36+
};
37+
}, [target, onIntersect, disabled]);
38+
}

0 commit comments

Comments
 (0)