diff --git a/.gitignore b/.gitignore
index 5ef6a52..8e6c13d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
# testing
/coverage
+example.http
# next.js
/.next/
diff --git a/components/Pagination/Pagination.tsx b/components/Pagination/Pagination.tsx
new file mode 100644
index 0000000..41d0a0c
--- /dev/null
+++ b/components/Pagination/Pagination.tsx
@@ -0,0 +1,110 @@
+import PaginationButton from './PaginationButton';
+
+interface PaginationProps {
+ totalCount: number;
+ currentPage: number;
+ pageSize: number;
+ onPageChange: (page: number) => void;
+}
+
+/**
+ * 페이지네이션 컴포넌트
+ * @param totalCount - 전체 데이터 개수
+ * @param currentPage - 현재 페이지 번호
+ * @param pageSize - 페이지 당 데이터 개수
+ * @param onPageChange - 페이지 변경 핸들러(상태 리프팅, setter 함수를 받음)
+ * @example
+ *
+ */
+export default function Pagination({
+ totalCount,
+ currentPage,
+ pageSize,
+ onPageChange,
+}: PaginationProps) {
+ const totalPages = Math.ceil(totalCount / pageSize);
+ // 노출 될 페이지네이션의 총 개수 (현재 5개만 노출)
+ const maxPages = 5;
+
+ // 이전 페이지로 이동할 때, 최소 1 페이지를 유지
+ const handlePrev = () => {
+ onPageChange(Math.max(currentPage - 1, 1));
+ };
+
+ // 다음 페이지로 이동할 때, 최대 totalPages 페이지를 유지
+ const handleNext = () => {
+ onPageChange(Math.min(currentPage + 1, totalPages));
+ };
+
+ // 총 페이지 번호를 계산하여 배열로 리턴하는 함수
+ const getPages = () => {
+ const pages = []; // 반환할 페이지 번호 배열
+ const half = Math.floor(maxPages / 2); // 현재 페이지 중심 버튼 배치 기준점(중앙 배치를 위해 /2)
+
+ let start = Math.max(currentPage - half, 1); // 시작 페이지 계산
+ let end = Math.min(start + maxPages - 1, totalPages); // 끝 페이지 계산
+
+ /**
+ * 시작 페이지 조정
+ * - end - start < maxPages - 1: 끝 페이지가 maxPages보다 작을 경우 조건 실행
+ * - (끝 페이지 - 총 페이지 + 1) 과 1 중 큰 값을 선택하여 시작 페이지 재할당
+ */
+ if (end - start < maxPages - 1) {
+ start = Math.max(end - maxPages + 1, 1);
+ }
+
+ // 페이지 배열 생성
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+
+ return pages;
+ };
+
+ const arrowStyles = 'h-[24px] w-[24px] mo:h-[18px] mo:w-[18px]';
+
+ return (
+
+ {/* 이전 페이지 버튼 - 현재 페이지가 1일 경우 disabled*/}
+
+
+
+
+ {/* 페이지 버튼 목록 */}
+
+ {getPages().map((pageNumber) => (
+
onPageChange(pageNumber)}
+ className={
+ pageNumber === currentPage ? 'text-green-200' : 'text-gray-400'
+ }
+ >
+ {pageNumber}
+
+ ))}
+
+
+ {/* 다음 페이지 버튼 - 현재 페이지가 totalPages와 같으면 disabled*/}
+
+
+
+
+ );
+}
diff --git a/components/Pagination/PaginationButton.tsx b/components/Pagination/PaginationButton.tsx
new file mode 100644
index 0000000..669baac
--- /dev/null
+++ b/components/Pagination/PaginationButton.tsx
@@ -0,0 +1,30 @@
+interface PaginationButtonProps {
+ children: React.ReactNode;
+ onClick: () => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+/**
+ * Pagination button component
+ * @param children - 버튼 컨텐츠(아이콘, 텍스트 등)
+ * @param onClick - 버튼 클릭 핸들러
+ * @param className - 커스텀 클래스
+ * @param disabled - 버튼 비활성화 여부
+ */
+export default function PaginationButton({
+ children,
+ onClick,
+ className,
+ disabled,
+}: PaginationButtonProps) {
+ return (
+
+ );
+}
diff --git a/pages/test/pagination.tsx b/pages/test/pagination.tsx
new file mode 100644
index 0000000..2f2c08a
--- /dev/null
+++ b/pages/test/pagination.tsx
@@ -0,0 +1,69 @@
+import { useEffect, useState } from 'react';
+
+import Pagination from '@/components/Pagination/Pagination';
+
+interface Post {
+ writer: {
+ name: string;
+ id: number;
+ };
+ title: string;
+ id: number;
+}
+
+/**
+ * 테스트를 위한 더미 데이터 생성
+ */
+const dummyData: Post[] = Array.from({ length: 100 }, (_, i) => ({
+ writer: {
+ name: `작성자 ${i + 1}`,
+ id: i + 1,
+ },
+ title: `게시글 제목 ${i + 1}`,
+ id: i + 1,
+}));
+
+export default function PaginationTest() {
+ const [data, setData] = useState([]); // 게시글 데이터
+ const [totalCount, setTotalCount] = useState(0); // 전체 페이지 카운트
+ const [currentPage, setCurrentPage] = useState(1); // 현재 페이지
+ const pageSize = 10; // 페이지 당 데이터 개수
+
+ // 실제 서버 요청 대신 더미 데이터 사용
+ useEffect(() => {
+ const fetchData = async () => {
+ // 페이지를 넘겼을때 시작 데이터 위치 계산
+ const startIndex = (currentPage - 1) * pageSize;
+ // 페이지를 넘겼을때 마지막 데이터 위치 계산
+ const endIndex = startIndex + pageSize;
+ // pageSize만큼 잘라서 데이터 노출
+ const currentData = dummyData.slice(startIndex, endIndex);
+ setData(currentData);
+ setTotalCount(dummyData.length);
+ };
+
+ fetchData();
+ }, [currentPage]);
+
+ return (
+ <>
+ {/* 더미 게시글 UI */}
+
+ {data.map((item) => (
+ -
+
{item.title}
+ {item.writer.name}
+
+ ))}
+
+
+ {/* Pagination 컴포넌트 사용 부분 */}
+
+ >
+ );
+}
diff --git a/public/icon/icon-arrow.svg b/public/icon/icon-arrow.svg
new file mode 100644
index 0000000..1daa1f5
--- /dev/null
+++ b/public/icon/icon-arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 942b0de..bc08901 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -51,6 +51,7 @@ export default {
},
boxShadow: {
custom: '0px 4px 20px 0px rgba(0, 0, 0, 0.08)',
+ 'custom-dark': '0px 4px 20px 0px rgba(255, 255, 255, 0.08)',
},
fontSize: {
// 12px