kcg-ai-monitoring/frontend/src/shared/components/common/Pagination.tsx
htlee 5812d9dea3 feat(frontend): UI 공통 인프라 + 19개 분류 카탈로그 구축
- 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) 시맨틱 토큰 적용
2026-04-08 10:53:40 +09:00

89 lines
3.2 KiB
TypeScript
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}