- cn() 유틸 신규 (clsx + tailwind-merge, 시맨틱 토큰 classGroup 등록) - theme.css @layer utilities로 직접 정의 (Tailwind v4 복합 이름 매핑 실패 대응): text-heading/label/hint/on-vivid/on-bright, bg-surface-raised/overlay - badgeVariants (CVA) 재구축: 8 intent x 4 size, rem 기반, !important 제거 - Badge 컴포넌트가 cn(badgeVariants, className)로 override 허용 - DataTable width 의미 변경: 고정 -> 선호 최소 너비 (minWidth), truncate + title 툴팁 - dateFormat.ts sv-SE 로케일로 YYYY-MM-DD HH:mm:ss 일관된 KST 출력 - ColorPicker 신규 (팔레트 + native color + hex 입력) - shared/constants/ 19개 카탈로그: violation/alert/event/enforcement/patrol/ engine/userRole/device/parentResolution/modelDeployment/gearGroup/darkVessel/ httpStatus/userAccount/loginResult/permission/vesselAnalysis/connection/trainingZone + kpiUiMap. 백엔드 enum/code_master 기반 SSOT - i18n ko/en common.json에 카테고리 섹션 추가 - adminApi.fetchRoles()가 updateRoleColorCache 자동 호출 - 공통 컴포넌트 (ExcelExport/NotificationBanner/Pagination/SaveButton) 시맨틱 토큰 적용
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||
|
||
/*
|
||
* SFR-02 공통컴포넌트: 페이지네이션
|
||
*/
|
||
|
||
interface PaginationProps {
|
||
page: number;
|
||
totalPages: number;
|
||
totalItems: number;
|
||
pageSize: number;
|
||
onPageChange: (page: number) => void;
|
||
}
|
||
|
||
export function Pagination({ page, totalPages, totalItems, pageSize, onPageChange }: PaginationProps) {
|
||
if (totalPages <= 1) return null;
|
||
|
||
const start = page * pageSize + 1;
|
||
const end = Math.min((page + 1) * pageSize, totalItems);
|
||
|
||
// 표시할 페이지 번호 범위
|
||
const range: number[] = [];
|
||
const maxVisible = 5;
|
||
let startPage = Math.max(0, page - Math.floor(maxVisible / 2));
|
||
const endPage = Math.min(totalPages - 1, startPage + maxVisible - 1);
|
||
if (endPage - startPage < maxVisible - 1) {
|
||
startPage = Math.max(0, endPage - maxVisible + 1);
|
||
}
|
||
for (let i = startPage; i <= endPage; i++) range.push(i);
|
||
|
||
return (
|
||
<div className="flex items-center justify-between px-1 py-2">
|
||
<span className="text-[10px] text-hint">
|
||
전체 {totalItems.toLocaleString()}건 중 {start.toLocaleString()}–{end.toLocaleString()}
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
onClick={() => onPageChange(0)}
|
||
disabled={page === 0}
|
||
className="p-1 rounded text-hint hover:text-heading hover:bg-surface-overlay disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||
title="처음"
|
||
>
|
||
<ChevronsLeft className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => onPageChange(page - 1)}
|
||
disabled={page === 0}
|
||
className="p-1 rounded text-hint hover:text-heading hover:bg-surface-overlay disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||
title="이전"
|
||
>
|
||
<ChevronLeft className="w-3.5 h-3.5" />
|
||
</button>
|
||
{range.map((p) => (
|
||
<button
|
||
key={p}
|
||
onClick={() => onPageChange(p)}
|
||
className={`min-w-[24px] h-6 px-1 rounded text-[10px] font-medium transition-colors ${
|
||
p === page
|
||
? 'bg-blue-600 text-on-vivid'
|
||
: 'text-muted-foreground hover:text-heading hover:bg-surface-overlay'
|
||
}`}
|
||
>
|
||
{p + 1}
|
||
</button>
|
||
))}
|
||
<button
|
||
onClick={() => onPageChange(page + 1)}
|
||
disabled={page >= totalPages - 1}
|
||
className="p-1 rounded text-hint hover:text-heading hover:bg-surface-overlay disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||
title="다음"
|
||
>
|
||
<ChevronRight className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => onPageChange(totalPages - 1)}
|
||
disabled={page >= totalPages - 1}
|
||
className="p-1 rounded text-hint hover:text-heading hover:bg-surface-overlay disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||
title="마지막"
|
||
>
|
||
<ChevronsRight className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
<span className="text-[10px] text-hint">
|
||
{page + 1} / {totalPages} 페이지
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|