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 */} + + + {/* 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