diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..b735e4f --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,145 @@ +interface PaginationProps { + page: number; + totalPages: number; + totalElements: number; + pageSize: number; + onPageChange: (page: number) => void; +} + +/** + * 표시할 페이지 번호 목록 생성 (Truncated Page Number) + * - 총 7슬롯 이하면 전부 표시 + * - 7슬롯 초과면 현재 페이지 기준 양쪽 1개 + 처음/끝 + ellipsis + */ +function getPageNumbers(current: number, total: number): (number | 'ellipsis')[] { + if (total <= 7) { + return Array.from({ length: total }, (_, i) => i); + } + + const pages: (number | 'ellipsis')[] = []; + const SIBLING = 1; + + const leftSibling = Math.max(current - SIBLING, 0); + const rightSibling = Math.min(current + SIBLING, total - 1); + + const showLeftEllipsis = leftSibling > 1; + const showRightEllipsis = rightSibling < total - 2; + + pages.push(0); + + if (showLeftEllipsis) { + pages.push('ellipsis'); + } else { + for (let i = 1; i < leftSibling; i++) { + pages.push(i); + } + } + + for (let i = leftSibling; i <= rightSibling; i++) { + if (i !== 0 && i !== total - 1) { + pages.push(i); + } + } + + if (showRightEllipsis) { + pages.push('ellipsis'); + } else { + for (let i = rightSibling + 1; i < total - 1; i++) { + pages.push(i); + } + } + + if (total > 1) { + pages.push(total - 1); + } + + return pages; +} + +export default function Pagination({ + page, + totalPages, + totalElements, + pageSize, + onPageChange, +}: PaginationProps) { + if (totalPages <= 1) return null; + + const start = page * pageSize + 1; + const end = Math.min((page + 1) * pageSize, totalElements); + const pages = getPageNumbers(page, totalPages); + + const btnBase = + 'inline-flex items-center justify-center w-7 h-7 text-xs rounded transition-colors'; + const btnEnabled = 'hover:bg-wing-hover text-wing-muted'; + const btnDisabled = 'opacity-30 cursor-not-allowed text-wing-muted'; + + return ( +
+ + {totalElements.toLocaleString()}건 중 {start.toLocaleString()}~ + {end.toLocaleString()} + +
+ {/* First */} + + {/* Prev */} + + + {/* Page Numbers */} + {pages.map((p, idx) => + p === 'ellipsis' ? ( + + … + + ) : ( + + ), + )} + + {/* Next */} + + {/* Last */} + +
+
+ ); +} diff --git a/frontend/src/pages/ExecutionDetail.tsx b/frontend/src/pages/ExecutionDetail.tsx index 34fd036..5a9a92a 100644 --- a/frontend/src/pages/ExecutionDetail.tsx +++ b/frontend/src/pages/ExecutionDetail.tsx @@ -6,6 +6,7 @@ import { usePoller } from '../hooks/usePoller'; import StatusBadge from '../components/StatusBadge'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import Pagination from '../components/Pagination'; const POLLING_INTERVAL_MS = 5000; @@ -92,7 +93,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { const fetchLogs = useCallback(async (p: number, s: ApiLogStatus) => { setLoading(true); try { - const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 50, status: s }); + const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 10, status: s }); setLogData(data); } catch { setLogData(null); @@ -163,7 +164,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { ) : logData && logData.content.length > 0 ? ( <> -
+
@@ -185,7 +186,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { key={log.logId} className={isError ? 'bg-red-50' : 'bg-white hover:bg-blue-50'} > - +
{page * 50 + idx + 1}{page * 10 + idx + 1}
@@ -225,33 +226,13 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) {
{/* 페이지네이션 */} - {logData.totalPages > 1 && ( -
- - {logData.totalElements.toLocaleString()}건 중{' '} - {(page * 50 + 1).toLocaleString()}~{Math.min((page + 1) * 50, logData.totalElements).toLocaleString()} - -
- - - {page + 1} / {logData.totalPages} - - -
-
- )} + ) : (

조회된 로그가 없습니다.

diff --git a/frontend/src/pages/RecollectDetail.tsx b/frontend/src/pages/RecollectDetail.tsx index 0524c19..3686f07 100644 --- a/frontend/src/pages/RecollectDetail.tsx +++ b/frontend/src/pages/RecollectDetail.tsx @@ -13,6 +13,7 @@ import { usePoller } from '../hooks/usePoller'; import StatusBadge from '../components/StatusBadge'; import EmptyState from '../components/EmptyState'; import LoadingSpinner from '../components/LoadingSpinner'; +import Pagination from '../components/Pagination'; const POLLING_INTERVAL_MS = 10_000; @@ -97,7 +98,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { const fetchLogs = useCallback(async (p: number, s: ApiLogStatus) => { setLoading(true); try { - const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 50, status: s }); + const data = await batchApi.getStepApiLogs(stepExecutionId, { page: p, size: 10, status: s }); setLogData(data); } catch { setLogData(null); @@ -168,7 +169,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { ) : logData && logData.content.length > 0 ? ( <> -
+
@@ -190,7 +191,7 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) { key={log.logId} className={isError ? 'bg-red-50' : 'bg-white hover:bg-blue-50'} > - +
{page * 50 + idx + 1}{page * 10 + idx + 1}
@@ -230,33 +231,13 @@ function ApiLogSection({ stepExecutionId, summary }: ApiLogSectionProps) {
{/* 페이지네이션 */} - {logData.totalPages > 1 && ( -
- - {logData.totalElements.toLocaleString()}건 중{' '} - {(page * 50 + 1).toLocaleString()}~{Math.min((page + 1) * 50, logData.totalElements).toLocaleString()} - -
- - - {page + 1} / {logData.totalPages} - - -
-
- )} + ) : (

조회된 로그가 없습니다.