kcg-ai-monitoring/frontend/src/features/admin/DataModelVerification.tsx
htlee c1cc36b134 refactor(design-system): 하드코딩 색상 라이트/다크 대응 + raw button/input 공통 컴포넌트 치환
30개 파일 전 영역에 동일한 패턴으로 SSOT 준수:

**StatBox 재설계 (2파일)**:
- RealGearGroups, RealVesselAnalysis 의 `color: string` prop 제거
- `intent: BadgeIntent` prop + `INTENT_TEXT_CLASS` 매핑 도입

**raw `<button>` → Button 컴포넌트 (다수)**:
- `bg-blue-600 hover:bg-blue-500 text-on-vivid ...` → `<Button variant="primary">`
- `bg-orange-600 ...` / `bg-green-600 ...` → `<Button variant="primary">`
- `bg-red-600 ...` → `<Button variant="destructive">`
- 아이콘 전용 → `<Button variant="ghost" aria-label=".." icon={...} />`
- detection/enforcement/admin/parent-inference/statistics/ai-operations/auth 전영역

**raw `<input>` → Input 컴포넌트**:
- parent-inference (ParentReview, ParentExclusion, LabelSession)
- admin (PermissionsPanel, UserRoleAssignDialog)
- ai-operations (AIAssistant)
- auth (LoginPage)

**raw `<select>` → Select 컴포넌트**:
- detection (RealGearGroups, RealVesselAnalysis, ChinaFishing)

**커스텀 탭 → TabBar/TabButton (segmented/underline)**:
- ChinaFishing: 모드 탭 + 선박 탭 + 통계 탭

**raw `<input type="checkbox">` → Checkbox**:
- GearDetection FilterCheckGroup

**하드코딩 Tailwind 색상 라이트/다크 쌍 변환 (전영역)**:
- `text-red-400` → `text-red-600 dark:text-red-400`
- `text-green-400` → `text-green-600 dark:text-green-400`
- blue/cyan/orange/yellow/purple/amber 동일 패턴
- `text-*-500` 아이콘도 `text-*-600 dark:text-*-500` 로 라이트 모드 대응
- 상태 dot (bg-red-500 animate-pulse 등)은 의도적 시각 구분이므로 유지

**에러 메시지 한글 → t('error.errorPrefix') 통일**:
- detection/parent-inference/admin 에서 `에러: {error}` 패턴 → `t('error.errorPrefix', { msg: error })`

**결과**: tsc 0 errors / eslint 0 errors (84 warnings 기존)
2026-04-16 17:09:14 +09:00

386 lines
25 KiB
TypeScript

import { useState } from 'react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { getStatusIntent } from '@shared/constants/statusIntent';
import { useAuth } from '@/app/auth/AuthContext';
import {
Database, CheckCircle, AlertTriangle, FileText, Users,
Layers, Table2, Search, ChevronRight, GitBranch,
Shield, Eye, ListChecks, ClipboardCheck,
} from 'lucide-react';
/*
* DAR-11: 데이터 모델 검증
*
* 설계된 데이터 모델을 설계자·개발자·해양경찰청·전문가 등이 참여하여
* 각 단계별 데이터 모델 검증 시 기준을 정의·실시한다.
*
* ① 검증 현황 ② 논리 모델 검증 ③ 물리 모델 검증 ④ 중복·정합성 점검 ⑤ 검증 결과 이력
*/
type Tab = 'overview' | 'logical' | 'physical' | 'duplication' | 'history';
// ─── 검증 현황 KPI ──────────────────
const VERIFICATION_KPI = [
{ label: '전체 테이블', value: '48개', icon: Table2, color: '#3b82f6' },
{ label: '논리 모델 검증', value: '완료', icon: GitBranch, color: '#10b981' },
{ label: '물리 모델 검증', value: '완료', icon: Database, color: '#8b5cf6' },
{ label: '중복 테이블', value: '0건', icon: Search, color: '#06b6d4' },
{ label: '미해결 이슈', value: '1건', icon: AlertTriangle, color: '#f59e0b' },
];
// ─── 검증 참여자 ──────────────────
const PARTICIPANTS = [
{ role: '데이터 설계자', name: '정해진 (주무관)', responsibility: '논리·물리 모델 설계, ERD 작성', badge: 'critical' as const },
{ role: '백엔드 개발자', name: '이상호 (경위)', responsibility: '마이그레이션 구현, 인덱스·성능 최적화', badge: 'info' as const },
{ role: '해양경찰청 담당관', name: '김영수 (사무관)', responsibility: '요구사항 대비 완전성 검토·승인', badge: 'warning' as const },
{ role: 'DB 전문가', name: '외부 자문위원', responsibility: '정규화·반정규화 타당성, 성능 검증', badge: 'success' as const },
];
// ─── 검증 절차 (4단계) ──────────────────
const VERIFICATION_PHASES = [
{ phase: '① 검증 계획 수립', actions: ['검증 범위·기준·일정 확정', '참여자 역할 분담', '체크리스트 작성'], responsible: '데이터 설계자', icon: FileText },
{ phase: '② 논리 모델 검증', actions: ['요구사항 대비 완전성 확인', '엔티티·속성·관계 정합성', '정규화 수준 적정성 검토'], responsible: '설계자 + 담당관', icon: GitBranch },
{ phase: '③ 물리 모델 검증', actions: ['테이블·컬럼·인덱스 구조 확인', '데이터 타입·제약조건 적정성', '성능 관점 반정규화 타당성'], responsible: '개발자 + DB전문가', icon: Database },
{ phase: '④ 결과 보고·조치', actions: ['검증 결과서 작성', '미해결 이슈 추적·조치', '최종 승인 및 이력 등록'], responsible: '전체 참여자', icon: ClipboardCheck },
];
// ─── 논리 모델 검증 항목 ──────────────────
const LOGICAL_CHECKS = [
{ category: '완전성', item: '요구사항 커버리지', desc: '48개 테이블이 SFR/DAR 전체 요구사항을 충족하는지 매핑 확인', result: '100% (48/48)', status: '통과' },
{ category: '완전성', item: '엔티티 누락 여부', desc: '식별된 비즈니스 영역(인증·탐지·단속·통계·관리) 대비 누락 엔티티 점검', result: '누락 0건', status: '통과' },
{ category: '정합성', item: '엔티티 관계 정의', desc: '외래키 관계, 참조 무결성, 카디널리티(1:N, M:N) 적정성', result: '78개 관계 확인', status: '통과' },
{ category: '정합성', item: '속성 정의 명확성', desc: '속성명 명명규칙, 도메인 정의, NULL 허용 정책 준수', result: '전체 준수', status: '통과' },
{ category: '정규화', item: '제3정규형 준수', desc: '함수 종속성 분석, 이행 종속 제거, 제3정규형(3NF) 이상 달성', result: '48/48 테이블', status: '통과' },
{ category: '정규화', item: '반정규화 타당성', desc: '성능 목적 반정규화 항목의 타당성 및 정합성 관리 방안', result: '3건 (타당)', status: '통과' },
{ category: '표준', item: '명명규칙 준수', desc: '테이블·컬럼·인덱스 명명규칙 (snake_case, kcg 스키마 접두어)', result: '전체 준수', status: '통과' },
{ category: '표준', item: '코드성 데이터 표준화', desc: '상태코드·유형코드 등 코드 마스터 테이블 일원화', result: 'code_master 통합', status: '통과' },
];
// ─── 물리 모델 검증 항목 ──────────────────
const PHYSICAL_CHECKS = [
{ category: '구조', item: '테이블 스페이스 배치', desc: 'PostgreSQL kcg 스키마 내 48개 테이블 배치 적정성', result: '적정', status: '통과' },
{ category: '구조', item: '파티셔닝 전략', desc: 'AIS 위치 로그 등 대용량 테이블 파티셔닝 적용 여부', result: 'TimescaleDB hypertable', status: '통과' },
{ category: '데이터 타입', item: '컬럼 타입 적정성', desc: 'VARCHAR 길이, NUMERIC 정밀도, TIMESTAMP 타임존 설정', result: '전체 적정', status: '통과' },
{ category: '데이터 타입', item: 'JSON vs 정규 컬럼', desc: 'JSONB 사용 항목의 타당성 (스키마 유연성 vs 쿼리 성능)', result: '3개 테이블 JSONB (타당)', status: '통과' },
{ category: '인덱스', item: '기본키·외래키 인덱스', desc: 'PK/FK 인덱스 자동 생성 확인, 복합키 순서 적정성', result: '전체 생성 확인', status: '통과' },
{ category: '인덱스', item: '조회 성능 인덱스', desc: '고빈도 조회 패턴 분석 기반 추가 인덱스 설계', result: '12개 추가 인덱스', status: '통과' },
{ category: '제약조건', item: 'NOT NULL·CHECK·UNIQUE', desc: '필수값 제약, 범위 체크, 유일성 제약 적용 현황', result: '전체 적용', status: '통과' },
{ category: '제약조건', item: '참조 무결성 (FK)', desc: '외래키 ON DELETE/UPDATE 정책 (CASCADE/RESTRICT)', result: '정책 수립 완료', status: '통과' },
{ category: '성능', item: '쿼리 실행 계획 검증', desc: '주요 조회 쿼리 EXPLAIN ANALYZE 검증', result: 'P95 < 100ms', status: '통과' },
{ category: '성능', item: '대량 INSERT 성능', desc: 'AIS 5분 주기 배치 INSERT 성능 검증 (bulk insert)', result: '10K rows/sec', status: '주의' },
];
// ─── 중복·정합성 점검 ──────────────────
const DUPLICATION_CHECKS = [
{ target: '테이블 중복', desc: '동일 목적의 테이블이 중복 존재하는지 점검', scope: '48개 테이블', result: '중복 0건', status: '통과' },
{ target: '컬럼 중복', desc: '동일 비즈니스 의미의 컬럼이 다른 이름으로 존재하는지 점검', scope: '전체 컬럼', result: '중복 0건', status: '통과' },
{ target: '반정규화 정합성', desc: '반정규화로 인한 중복 데이터의 동기화 방안 확인', scope: '3개 반정규화 항목', result: '트리거 기반 동기화', status: '통과' },
{ target: '코드값 일관성', desc: '상태코드·유형코드가 code_master 통해 일원 관리되는지 확인', scope: '19개 코드 카탈로그', result: '전체 일원화', status: '통과' },
{ target: '외래키 정합성', desc: '참조 대상 레코드 삭제 시 고아 레코드 발생 여부 점검', scope: '78개 FK 관계', result: '고아 0건', status: '통과' },
{ target: '스키마 간 정합성', desc: 'prediction 분석 결과 → kcgaidb 연계 시 데이터 타입 일치', scope: 'SNPDB ↔ kcgaidb', result: '타입 일치 확인', status: '통과' },
];
// ─── 데이터 주제영역 ──────────────────
const SUBJECT_AREAS = [
{ area: '인증·권한', tables: 'auth_user, auth_role, auth_perm_tree, auth_perm, auth_user_role', count: 5, desc: '사용자·역할·권한 트리 기반 RBAC' },
{ area: 'AIS·선박', tables: 'vessel_master, ais_position, vessel_permit', count: 3, desc: 'AIS 수신 데이터, 선박 기본정보, 허가 현황' },
{ area: '탐지·분석', tables: 'vessel_analysis, dark_vessel, gear_detection, transship_detection, ...', count: 12, desc: '14개 알고리즘 분석 결과 저장' },
{ area: '단속·이벤트', tables: 'prediction_event, enforcement_plan, enforcement_record, ...', count: 8, desc: '이벤트 발생·단속 계획·단속 이력' },
{ area: '모선 워크플로우', tables: 'parent_review, parent_exclusion, label_session, ...', count: 5, desc: '모선 확정·제외·학습 워크플로우' },
{ area: '순찰·함정', tables: 'patrol_ship, patrol_route, fleet_optimization, ...', count: 6, desc: '함정 관리·순찰 경로·최적화' },
{ area: '통계·감사', tables: 'statistics_daily, audit_log, access_log, login_history, ...', count: 5, desc: '통계 집계·감사 로그·접근 이력' },
{ area: '시스템·관리', tables: 'code_master, zone_polygon, gear_type_master, notice, ...', count: 4, desc: '코드 마스터·구역 정보·공지사항' },
];
// ─── 검증 결과 이력 ──────────────────
const VERIFICATION_HISTORY = [
{ id: 'VER-2026-005', date: '2026-04-10', phase: '물리 모델 검증', reviewer: '이상호, 외부 자문', target: 'V015 NUMERIC 정밀도 조정', issues: 0, result: '통과' },
{ id: 'VER-2026-004', date: '2026-04-07', phase: '중복·정합성 점검', reviewer: '정해진', target: '반정규화 3건 정합성', issues: 0, result: '통과' },
{ id: 'VER-2026-003', date: '2026-04-03', phase: '논리 모델 검증', reviewer: '김영수, 정해진', target: 'V014 함정·예측 테이블 추가', issues: 1, result: '조건부 통과' },
{ id: 'VER-2026-002', date: '2026-03-28', phase: '물리 모델 검증', reviewer: '이상호', target: 'V012~V013 이벤트·단속 테이블', issues: 0, result: '통과' },
{ id: 'VER-2026-001', date: '2026-03-20', phase: '논리 모델 검증', reviewer: '전체 참여', target: '초기 V001~V011 전체 구조', issues: 3, result: '조건부 통과' },
{ id: 'VER-2025-012', date: '2025-12-15', phase: '검증 계획 수립', reviewer: '김영수', target: '검증 계획서 v1.0 확정', issues: 0, result: '승인' },
];
export function DataModelVerification() {
const [tab, setTab] = useState<Tab>('overview');
const { hasPermission } = useAuth();
// 향후 Phase 3 에서 검증 승인·이력 등록 버튼 추가 시 가드로 연결
void hasPermission('admin:data-model-verification', 'CREATE');
void hasPermission('admin:data-model-verification', 'UPDATE');
const TABS: Array<{ key: Tab; icon: typeof Eye; label: string }> = [
{ key: 'overview', icon: Eye, label: '검증 현황' },
{ key: 'logical', icon: GitBranch, label: '논리 모델 검증' },
{ key: 'physical', icon: Database, label: '물리 모델 검증' },
{ key: 'duplication', icon: Search, label: '중복·정합성 점검' },
{ key: 'history', icon: FileText, label: '검증 결과 이력' },
];
return (
<PageContainer>
<PageHeader
icon={ListChecks}
iconColor="text-green-600 dark:text-green-400"
title="데이터 모델 검증"
description="DAR-11 | 논리·물리 데이터 모델 검증 기준 정의·실시 및 결과 관리"
demo
/>
<TabBar variant="underline">
{TABS.map(t => (
<TabButton key={t.key} variant="underline" active={tab === t.key}
icon={<t.icon className="w-3.5 h-3.5" />} onClick={() => setTab(t.key)}>
{t.label}
</TabButton>
))}
</TabBar>
{/* ── ① 검증 현황 ── */}
{tab === 'overview' && (
<div className="space-y-3">
<div className="flex gap-2">
{VERIFICATION_KPI.map(k => (
<div key={k.label} className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card" style={{ borderLeftColor: k.color, borderLeftWidth: 3 }}>
<k.icon className="w-5 h-5" style={{ color: k.color }} />
<div>
<div className="text-lg font-bold" style={{ color: k.color }}>{k.value}</div>
<div className="text-[9px] text-hint">{k.label}</div>
</div>
</div>
))}
</div>
{/* 검증 절차 */}
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<ClipboardCheck className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-[12px] font-bold text-heading"> (4)</span>
</div>
<div className="grid grid-cols-4 gap-3">
{VERIFICATION_PHASES.map((s, i) => (
<div key={s.phase} className="relative">
{i < VERIFICATION_PHASES.length - 1 && (
<ChevronRight className="absolute -right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-border z-10" />
)}
<div className="bg-surface-overlay rounded-lg p-3 h-full">
<div className="flex items-center gap-2 mb-2">
<s.icon className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-[11px] font-bold text-green-600 dark:text-green-400">{s.phase}</span>
</div>
<div className="text-[9px] text-cyan-600 dark:text-cyan-400 mb-2">{s.responsible}</div>
<ul className="space-y-1.5">
{s.actions.map(a => (
<li key={a} className="flex items-start gap-1.5 text-[9px] text-muted-foreground">
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-500 shrink-0 mt-0.5" />
<span>{a}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</CardContent></Card>
{/* 참여자 + 주제영역 */}
<div className="grid grid-cols-2 gap-3">
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Users className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<span className="text-[12px] font-bold text-heading"> </span>
</div>
<div className="space-y-2">
{PARTICIPANTS.map(p => (
<div key={p.role} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Badge intent={p.badge} size="sm">{p.role}</Badge>
<div className="flex-1">
<div className="text-[11px] text-heading font-medium">{p.name}</div>
<div className="text-[9px] text-hint">{p.responsibility}</div>
</div>
</div>
))}
</div>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Layers className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span className="text-[12px] font-bold text-heading"> ({SUBJECT_AREAS.reduce((s, a) => s + a.count, 0)} )</span>
</div>
<div className="space-y-1.5">
{SUBJECT_AREAS.map(a => (
<div key={a.area} className="flex items-center gap-2 px-2 py-1.5 bg-surface-overlay rounded text-[10px]">
<Badge intent="muted" size="xs">{a.count}</Badge>
<span className="text-heading font-medium w-24 shrink-0">{a.area}</span>
<span className="text-hint text-[9px] truncate flex-1">{a.desc}</span>
</div>
))}
</div>
</CardContent></Card>
</div>
</div>
)}
{/* ── ② 논리 모델 검증 ── */}
{tab === 'logical' && (
<div className="space-y-3">
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<GitBranch className="w-4 h-4 text-green-600 dark:text-green-400" />
<span className="text-[12px] font-bold text-heading"> </span>
<Badge intent="success" size="xs">{LOGICAL_CHECKS.filter(c => c.status === '통과').length}/{LOGICAL_CHECKS.length} </Badge>
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th>
<th className="text-left py-2"> </th>
<th className="text-left py-2"> </th>
<th className="text-center py-2"> </th>
<th className="text-center py-2"></th>
</tr>
</thead>
<tbody>
{LOGICAL_CHECKS.map(c => (
<tr key={c.item} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2"><Badge intent="muted" size="xs">{c.category}</Badge></td>
<td className="py-2.5 text-heading font-medium">{c.item}</td>
<td className="py-2.5 text-muted-foreground text-[9px]">{c.desc}</td>
<td className="py-2.5 text-center text-cyan-600 dark:text-cyan-400 font-medium text-[9px]">{c.result}</td>
<td className="py-2.5 text-center"><Badge intent={getStatusIntent(c.status)} size="sm">{c.status}</Badge></td>
</tr>
))}
</tbody>
</table>
</CardContent></Card>
<Card><CardContent className="p-4">
<div className="text-[12px] font-bold text-heading mb-3"> </div>
<div className="grid grid-cols-4 gap-3">
{SUBJECT_AREAS.map(a => (
<div key={a.area} className="bg-surface-overlay rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-heading">{a.area}</span>
<Badge intent="info" size="xs">{a.count}</Badge>
</div>
<div className="text-[8px] text-hint mb-1.5">{a.desc}</div>
<div className="text-[8px] text-muted-foreground font-mono leading-relaxed">{a.tables}</div>
</div>
))}
</div>
</CardContent></Card>
</div>
)}
{/* ── ③ 물리 모델 검증 ── */}
{tab === 'physical' && (
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Database className="w-4 h-4 text-purple-600 dark:text-purple-400" />
<span className="text-[12px] font-bold text-heading"> </span>
<Badge intent="success" size="xs">{PHYSICAL_CHECKS.filter(c => c.status === '통과').length}/{PHYSICAL_CHECKS.length} </Badge>
{PHYSICAL_CHECKS.some(c => c.status === '주의') && (
<Badge intent="warning" size="xs">{PHYSICAL_CHECKS.filter(c => c.status === '주의').length} </Badge>
)}
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"></th>
<th className="text-left py-2"> </th>
<th className="text-left py-2"> </th>
<th className="text-center py-2"> </th>
<th className="text-center py-2"></th>
</tr>
</thead>
<tbody>
{PHYSICAL_CHECKS.map(c => (
<tr key={c.item} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2"><Badge intent="muted" size="xs">{c.category}</Badge></td>
<td className="py-2.5 text-heading font-medium">{c.item}</td>
<td className="py-2.5 text-muted-foreground text-[9px]">{c.desc}</td>
<td className="py-2.5 text-center text-cyan-600 dark:text-cyan-400 font-medium text-[9px]">{c.result}</td>
<td className="py-2.5 text-center"><Badge intent={getStatusIntent(c.status)} size="sm">{c.status}</Badge></td>
</tr>
))}
</tbody>
</table>
</CardContent></Card>
)}
{/* ── ④ 중복·정합성 점검 ── */}
{tab === 'duplication' && (
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
<span className="text-[12px] font-bold text-heading"> · </span>
<Badge intent="success" size="xs">{DUPLICATION_CHECKS.filter(c => c.status === '통과').length}/{DUPLICATION_CHECKS.length} </Badge>
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"> </th>
<th className="text-left py-2"> </th>
<th className="text-center py-2"> </th>
<th className="text-center py-2"></th>
<th className="text-center py-2"></th>
</tr>
</thead>
<tbody>
{DUPLICATION_CHECKS.map(c => (
<tr key={c.target} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2 text-heading font-medium">{c.target}</td>
<td className="py-2.5 text-muted-foreground text-[9px]">{c.desc}</td>
<td className="py-2.5 text-center"><Badge intent="muted" size="xs">{c.scope}</Badge></td>
<td className="py-2.5 text-center text-cyan-600 dark:text-cyan-400 font-medium text-[9px]">{c.result}</td>
<td className="py-2.5 text-center"><Badge intent={getStatusIntent(c.status)} size="sm">{c.status}</Badge></td>
</tr>
))}
</tbody>
</table>
</CardContent></Card>
)}
{/* ── ⑤ 검증 결과 이력 ── */}
{tab === 'history' && (
<Card><CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<FileText className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<span className="text-[12px] font-bold text-heading"> </span>
<Badge intent="info" size="xs">{VERIFICATION_HISTORY.length}</Badge>
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-hint border-b border-border">
<th className="text-left py-2 px-2"> ID</th>
<th className="text-center py-2"></th>
<th className="text-center py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-center py-2"></th>
<th className="text-center py-2"></th>
</tr>
</thead>
<tbody>
{VERIFICATION_HISTORY.map(h => (
<tr key={h.id} className="border-b border-border/50 hover:bg-surface-overlay transition-colors">
<td className="py-2.5 px-2 text-hint font-mono text-[9px]">{h.id}</td>
<td className="py-2.5 text-center text-muted-foreground">{h.date}</td>
<td className="py-2.5 text-center"><Badge intent="muted" size="xs">{h.phase}</Badge></td>
<td className="py-2.5 text-muted-foreground">{h.reviewer}</td>
<td className="py-2.5 text-heading font-medium text-[9px]">{h.target}</td>
<td className="py-2.5 text-center">{h.issues > 0 ? <span className="text-yellow-600 dark:text-yellow-400 font-bold">{h.issues}</span> : <span className="text-green-600 dark:text-green-400">0</span>}</td>
<td className="py-2.5 text-center"><Badge intent={getStatusIntent(h.result)} size="sm">{h.result}</Badge></td>
</tr>
))}
</tbody>
</table>
</CardContent></Card>
)}
</PageContainer>
);
}