KCG AI 기반 불법조업 탐지·차단 플랫폼 프론트엔드. React 19 + TypeScript 5.9 + Vite 8 + MapLibre + deck.gl + Zustand + Tailwind CSS. SFR 20개 전체 UI 구현 완료, 백엔드 연동 대기. - npm + Nexus 프록시 레지스트리 설정 - 팀 워크플로우 v1.6.1 부트스트랩 파일 배치 - .githooks (commit-msg, post-checkout) - package.json name: kcg-ai-monitoring v0.1.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
49 lines
3.4 KiB
TypeScript
49 lines
3.4 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Card, CardContent } from '@shared/components/ui/card';
|
|
import { Badge } from '@shared/components/ui/badge';
|
|
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
|
import { FileText, Ship, MapPin, Calendar, Shield, CheckCircle, XCircle } from 'lucide-react';
|
|
import { useEnforcementStore } from '@stores/enforcementStore';
|
|
|
|
/* SFR-11: 단속·탐지 이력 관리 */
|
|
|
|
interface Record { id: string; date: string; zone: string; vessel: string; violation: string; action: string; aiMatch: string; result: string; [key: string]: unknown; }
|
|
const cols: DataColumn<Record>[] = [
|
|
{ key: 'id', label: 'ID', width: '80px', render: v => <span className="text-hint font-mono text-[10px]">{v as string}</span> },
|
|
{ key: 'date', label: '일시', width: '130px', sortable: true, render: v => <span className="text-muted-foreground font-mono text-[10px]">{v as string}</span> },
|
|
{ key: 'zone', label: '해역', width: '90px', sortable: true },
|
|
{ key: 'vessel', label: '대상 선박', sortable: true, render: v => <span className="text-cyan-400 font-medium">{v as string}</span> },
|
|
{ key: 'violation', label: '위반 내용', width: '100px', sortable: true, render: v => <Badge className="bg-red-500/15 text-red-400 border-0 text-[9px]">{v as string}</Badge> },
|
|
{ key: 'action', label: '조치', width: '90px' },
|
|
{ key: 'aiMatch', label: 'AI 매칭', width: '70px', align: 'center',
|
|
render: v => { const m = v as string; return m === '일치' ? <CheckCircle className="w-3.5 h-3.5 text-green-400 inline" /> : <XCircle className="w-3.5 h-3.5 text-red-400 inline" />; } },
|
|
{ key: 'result', label: '결과', width: '80px', align: 'center', sortable: true,
|
|
render: v => { const r = v as string; const c = r.includes('처벌') || r.includes('수사') ? 'bg-red-500/20 text-red-400' : r.includes('오탐') ? 'bg-muted text-muted-foreground' : 'bg-yellow-500/20 text-yellow-400'; return <Badge className={`border-0 text-[9px] ${c}`}>{r}</Badge>; } },
|
|
];
|
|
|
|
export function EnforcementHistory() {
|
|
const { t } = useTranslation('enforcement');
|
|
const { records, load } = useEnforcementStore();
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
const DATA: Record[] = records as Record[];
|
|
|
|
return (
|
|
<div className="p-5 space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2"><FileText className="w-5 h-5 text-blue-400" />{t('history.title')}</h2>
|
|
<p className="text-[10px] text-hint mt-0.5">{t('history.desc')}</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{[{ l: '총 단속', v: DATA.length, c: 'text-heading' }, { l: '처벌', v: DATA.filter(d => d.result.includes('처벌')).length, c: 'text-red-400' }, { l: 'AI 일치', v: DATA.filter(d => d.aiMatch === '일치').length, c: 'text-green-400' }, { l: '오탐', v: DATA.filter(d => d.result.includes('오탐')).length, c: 'text-yellow-400' }].map(k => (
|
|
<div key={k.l} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
|
<span className={`text-base font-bold ${k.c}`}>{k.v}</span><span className="text-[9px] text-hint">{k.l}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<DataTable data={DATA} columns={cols} pageSize={10} searchPlaceholder="선박명, 해역, 위반내용 검색..." searchKeys={['vessel', 'zone', 'violation', 'result']} exportFilename="단속이력" />
|
|
</div>
|
|
);
|
|
}
|