Skip to content

Commit 5c936ee

Browse files
authored
Merge pull request #34 from CodeitPart3/COMPONENT-33-JIN
[feat] Pagination 컴포넌트 구현
2 parents 77e70fc + 01f930d commit 5c936ee

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

src/assets/icon/arrow-left.svg

Lines changed: 1 addition & 1 deletion
Loading

src/assets/icon/arrow-right.svg

Lines changed: 1 addition & 1 deletion
Loading

src/components/Pagination.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useSearchParams } from "react-router-dom";
2+
3+
import { ArrowLeft, ArrowRight } from "@/assets/icon";
4+
import { cn } from "@/utils/cn";
5+
6+
interface PaginationProps {
7+
/** 전체 아이템 개수 */
8+
count: number;
9+
/** 한 페이지에 보여줄 아이템 개수 */
10+
itemCountPerPage: number;
11+
/** 노출할 페이지 버튼 개수 (기본 7) – 홀수를 권장 */
12+
limit?: number;
13+
}
14+
15+
export default function Pagination({
16+
count,
17+
itemCountPerPage,
18+
limit = 7,
19+
}: PaginationProps) {
20+
const [searchParams, setSearchParams] = useSearchParams();
21+
22+
const currentPage = Number(searchParams.get("page")) || 1;
23+
const totalPages = Math.max(1, Math.ceil(count / itemCountPerPage));
24+
25+
// limit 이 전체 페이지보다 크면 전체 페이지 수만큼만 사용
26+
const visibleCount = Math.min(limit, totalPages);
27+
const half = Math.floor(visibleCount / 2);
28+
29+
/**
30+
* start/end 계산 – currentPage 를 중앙(slot index = half)에 배치.
31+
* 가장 앞/뒤 구간에서는 남는 슬롯을 앞이나 뒤로 몰아넣어 빈칸이 생기지 않도록 보정합니다.
32+
*/
33+
let start = currentPage - half;
34+
if (start < 1) start = 1;
35+
let end = start + visibleCount - 1;
36+
if (end > totalPages) {
37+
end = totalPages;
38+
start = Math.max(1, end - visibleCount + 1);
39+
}
40+
41+
const pages = Array.from({ length: visibleCount }, (_, i) => start + i);
42+
43+
const goToPage = (page: number) => {
44+
if (page < 1 || page > totalPages || page === currentPage) return;
45+
const next = new URLSearchParams(searchParams);
46+
next.set("page", String(page));
47+
setSearchParams(next, { replace: true });
48+
};
49+
50+
const isTotalPagesMoreThanLimit = totalPages > limit;
51+
const isFirstPage = currentPage === 1;
52+
const isLastPage = currentPage === totalPages;
53+
54+
return (
55+
<ul className="flex items-center md:gap-1 sm:gap-0.5 select-none text-black">
56+
{isTotalPagesMoreThanLimit && (
57+
<li className="flex items-center mr-4">
58+
<button
59+
onClick={() => goToPage(currentPage - 1)}
60+
disabled={isFirstPage}
61+
aria-label="이전 페이지"
62+
className="active:scale-95 transition-transform cursor-pointer disabled:cursor-default"
63+
>
64+
<ArrowLeft
65+
className={cn(
66+
"w-5 h-5",
67+
isFirstPage ? "fill-gray-40" : "fill-black",
68+
)}
69+
/>
70+
</button>
71+
</li>
72+
)}
73+
74+
{pages.map((page, idx) => {
75+
const isActive = page === currentPage;
76+
return (
77+
<li key={idx}>
78+
<button
79+
onClick={() => goToPage(page)}
80+
disabled={isActive}
81+
aria-current={isActive ? "page" : undefined}
82+
className={cn(
83+
"md:w-10 md:h-10 sm:w-8 sm:h-8 grid place-content-center rounded-sm md:text-sm sm:text-xs",
84+
"cursor-pointer disabled:cursor-default transition-colors duration-150 active:scale-95",
85+
isActive ? "bg-red-30 text-white" : "hover:bg-gray-100",
86+
)}
87+
>
88+
{page}
89+
</button>
90+
</li>
91+
);
92+
})}
93+
94+
{isTotalPagesMoreThanLimit && !isLastPage && (
95+
<li className="flex items-center ml-4">
96+
<button
97+
onClick={() => goToPage(currentPage + 1)}
98+
aria-label="다음 페이지"
99+
className="active:scale-95 transition-transform cursor-pointer disabled:cursor-default"
100+
>
101+
<ArrowRight className={cn("w-5 h-5 fill-black")} />
102+
</button>
103+
</li>
104+
)}
105+
</ul>
106+
);
107+
}

0 commit comments

Comments
 (0)