mda-snp-batch 기반으로 snp-sync-batch 프로젝트 생성 - 프론트엔드: Thymeleaf → React + TypeScript + Vite + Tailwind CSS 전환 - 컨텍스트: /snp-sync, 포트 8051 - 재수집(Recollection) 관련 코드 제거 - displayName → job_schedule.description 기반으로 전환 - 누락 API 추가 (statistics, jobs/detail, executions/recent) - 실행 이력 조회 속도 개선 (JDBC 경량 쿼리) - 스케줄 CRUD API 메서드 매핑 수정 (PUT/DELETE) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
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 (
|
|
<div className="flex items-center justify-between mt-2 text-xs text-wing-muted">
|
|
<span>
|
|
{totalElements.toLocaleString()}건 중 {start.toLocaleString()}~
|
|
{end.toLocaleString()}
|
|
</span>
|
|
<div className="flex items-center gap-0.5">
|
|
{/* First */}
|
|
<button
|
|
onClick={() => onPageChange(0)}
|
|
disabled={page === 0}
|
|
className={`${btnBase} ${page === 0 ? btnDisabled : btnEnabled}`}
|
|
title="처음"
|
|
>
|
|
«
|
|
</button>
|
|
{/* Prev */}
|
|
<button
|
|
onClick={() => onPageChange(page - 1)}
|
|
disabled={page === 0}
|
|
className={`${btnBase} ${page === 0 ? btnDisabled : btnEnabled}`}
|
|
title="이전"
|
|
>
|
|
‹
|
|
</button>
|
|
|
|
{/* Page Numbers */}
|
|
{pages.map((p, idx) =>
|
|
p === 'ellipsis' ? (
|
|
<span key={`e-${idx}`} className="w-7 h-7 inline-flex items-center justify-center text-wing-muted">
|
|
…
|
|
</span>
|
|
) : (
|
|
<button
|
|
key={p}
|
|
onClick={() => onPageChange(p)}
|
|
className={`${btnBase} ${
|
|
p === page
|
|
? 'bg-wing-accent text-white font-semibold'
|
|
: btnEnabled
|
|
}`}
|
|
>
|
|
{p + 1}
|
|
</button>
|
|
),
|
|
)}
|
|
|
|
{/* Next */}
|
|
<button
|
|
onClick={() => onPageChange(page + 1)}
|
|
disabled={page >= totalPages - 1}
|
|
className={`${btnBase} ${page >= totalPages - 1 ? btnDisabled : btnEnabled}`}
|
|
title="다음"
|
|
>
|
|
›
|
|
</button>
|
|
{/* Last */}
|
|
<button
|
|
onClick={() => onPageChange(totalPages - 1)}
|
|
disabled={page >= totalPages - 1}
|
|
className={`${btnBase} ${page >= totalPages - 1 ? btnDisabled : btnEnabled}`}
|
|
title="마지막"
|
|
>
|
|
»
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|