kcg-ai-monitoring/frontend/src/features/detection/ChinaFishing.tsx
htlee 19b1613157 feat: 프론트 전수 mock 정리 + UTC→KST 통일 + i18n 수정 + stats hourly API
## 시간 표시 KST 통일
- shared/utils/dateFormat.ts 공통 유틸 신규 (formatDateTime/formatDate/formatTime/toDateParam)
- 14개 파일에서 인라인 toLocaleString → 공통 유틸 교체

## i18n 'group.parentInference' 사이드바 미번역 수정
- ko/en common.json의 'group' 키 중복 정의를 병합
  (95행 두번째 group 객체가 35행을 덮어써서 parentInference 누락)

## Dashboard/MonitoringDashboard/Statistics 더미→실 API
- 백엔드 GET /api/stats/hourly 신규 (PredictionStatsHourly 엔티티/리포지토리)
- Dashboard: HOURLY_DETECTION/VESSEL_TYPE/AREA_RISK 하드코딩 제거 →
  getHourlyStats(24) + getDailyStats(today) 결과로 useMemo 변환
- MonitoringDashboard: TREND Math.random() 제거 → getHourlyStats 기반
  위험도 가중평균 + 경보 카운트
- Statistics: KPI_DATA 하드코딩 제거 → getKpiMetrics() 결과를 표 행으로

## Store mock 의존성 제거
- eventStore.alerts/MOCK_ALERTS 제거 (MobileService는 events에서 직접 추출)
- enforcementStore.plans 제거 (EnforcementPlan은 이미 직접 API 호출)
- transferStore + MOCK_TRANSFERS 완전 제거
  (ChinaFishing/TransferDetection은 RealTransshipSuspects 컴포넌트 사용)
- mock/events.ts, mock/enforcement.ts, mock/transfers.ts 파일 삭제

## RiskMap 랜덤 격자 제거
- generateGrid() Math.random() 제거 → 빈 배열 + 'AI 분석 데이터 수집 중' 안내
- MTIS 외부 통계 5개 탭에 [MTIS 외부 통계] 배지 추가

## 12개 mock 화면에 '데모 데이터' 노란색 배지 추가
- patrol/PatrolRoute, FleetOptimization
- admin/AdminPanel, DataHub, NoticeManagement, SystemConfig
- ai-operations/AIModelManagement, MLOpsPage
- field-ops/ShipAgent
- statistics/ReportManagement, ExternalService
- surveillance/MapControl

## 백엔드 NUMERIC precision 동기화
- PredictionKpi.deltaPct: 5,2 → 12,2
- PredictionStatsDaily/Monthly.aiAccuracyPct: 5,2 → 12,2
- (V015 마이그레이션과 동기화)

44 files changed, +346 / -787

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:36:38 +09:00

677 lines
31 KiB
TypeScript

import { useState, useEffect, useMemo, useCallback } from 'react';
import { Card, CardContent } from '@shared/components/ui/card';
import { Badge } from '@shared/components/ui/badge';
import {
Search, Ship, Clock, ChevronRight, ChevronLeft, Cloud,
Eye, AlertTriangle, Radio, RotateCcw,
MapPin, Brain, RefreshCw, Crosshair as CrosshairIcon, Loader2
} from 'lucide-react';
import { formatDateTime } from '@shared/utils/dateFormat';
import { GearIdentification } from './GearIdentification';
import { RealAllVessels, RealTransshipSuspects } from './RealVesselAnalysis';
import { PieChart as EcPieChart } from '@lib/charts';
import {
fetchVesselAnalysis,
filterDarkVessels,
filterTransshipSuspects,
type VesselAnalysisItem,
type VesselAnalysisStats,
} from '@/services/vesselAnalysisApi';
// ─── 중국 MMSI prefix ─────────────
const CHINA_MMSI_PREFIX = '412';
function isChinaVessel(mmsi: string): boolean {
return mmsi.startsWith(CHINA_MMSI_PREFIX);
}
// ─── 특이운항 선박 리스트 타입 ────────────────
type VesselStatus = '의심' | '양호' | '경고';
interface VesselItem {
id: string;
mmsi: string;
callSign: string;
channel: string;
source: string;
name: string;
type: string;
country: string;
status: VesselStatus;
riskPct: number;
}
function deriveVesselStatus(score: number): VesselStatus {
if (score >= 70) return '경고';
if (score >= 40) return '의심';
return '양호';
}
function mapToVesselItem(item: VesselAnalysisItem, idx: number): VesselItem {
const score = item.algorithms.riskScore.score;
return {
id: String(idx + 1),
mmsi: item.mmsi,
callSign: '-',
channel: '',
source: 'AIS',
name: item.classification.vesselType || item.mmsi,
type: item.classification.fishingPct > 0.5 ? 'Fishing' : 'Cargo',
country: 'China',
status: deriveVesselStatus(score),
riskPct: score,
};
}
// ─── VTS 연계 항목 ─────────────────────
const VTS_ITEMS = [
{ name: '경인연안', active: true },
{ name: '평택항', active: false },
{ name: '경인항', active: false },
{ name: '대산항', active: true },
{ name: '인천항', active: true },
{ name: '태안연안', active: false },
];
// ─── 환적 탐지 뷰: RealTransshipSuspects 컴포넌트 사용 ───
// ─── 서브 컴포넌트 ─────────────────────
function SemiGauge({ value, label, color }: { value: number; label: string; color: string }) {
const angle = (value / 100) * 180;
return (
<div className="flex flex-col items-center">
<div className="relative w-28 h-16 overflow-hidden">
<svg viewBox="0 0 120 65" className="w-full h-full">
{/* 배경 호 */}
<path d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke="#1e293b" strokeWidth="10" strokeLinecap="round" />
{/* 값 호 */}
<path
d="M 10 60 A 50 50 0 0 1 110 60"
fill="none"
stroke={color}
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={`${(angle / 180) * 157} 157`}
/>
</svg>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 text-center">
<span className="text-xl font-extrabold text-heading">{value.toFixed(2)}</span>
<span className="text-xs text-muted-foreground ml-0.5">%</span>
</div>
</div>
<span className="text-[10px] text-muted-foreground mt-1">{label}</span>
</div>
);
}
function CircleGauge({ value, label }: { value: number; label: string }) {
const circumference = 2 * Math.PI * 42;
const offset = circumference - (value / 100) * circumference;
return (
<div className="flex flex-col items-center">
<div className="relative w-24 h-24">
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90">
<circle cx="50" cy="50" r="42" fill="none" stroke="#1e293b" strokeWidth="8" />
<circle
cx="50" cy="50" r="42" fill="none"
stroke="#10b981" strokeWidth="8" strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-extrabold text-heading">{value}</span>
<span className="text-[10px] text-muted-foreground">%</span>
</div>
</div>
<span className="text-[10px] text-muted-foreground mt-1">{label}</span>
</div>
);
}
function StatusRing({ status, riskPct }: { status: VesselStatus; riskPct: number }) {
const colors: Record<VesselStatus, { ring: string; bg: string; text: string }> = {
'의심': { ring: '#f97316', bg: 'bg-orange-500/10', text: 'text-orange-400' },
'양호': { ring: '#10b981', bg: 'bg-green-500/10', text: 'text-green-400' },
'경고': { ring: '#ef4444', bg: 'bg-red-500/10', text: 'text-red-400' },
};
const c = colors[status];
const circumference = 2 * Math.PI * 18;
const offset = circumference - (riskPct / 100) * circumference;
return (
<div className="relative w-12 h-12 shrink-0">
<svg viewBox="0 0 44 44" className="w-full h-full -rotate-90">
<circle cx="22" cy="22" r="18" fill="none" stroke="#1e293b" strokeWidth="3" />
<circle cx="22" cy="22" r="18" fill="none" stroke={c.ring} strokeWidth="3" strokeLinecap="round"
strokeDasharray={circumference} strokeDashoffset={offset} />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={`text-[8px] font-bold ${c.text}`}>{status}</span>
<span className="text-[9px] font-bold text-heading">{riskPct}%</span>
</div>
</div>
);
}
// ─── 메인 페이지 ──────────────────────
// ─── 환적 탐지 뷰 ─────────────────────
function TransferView() {
return (
<div className="space-y-4">
<div>
<h2 className="text-base font-bold text-heading">· </h2>
<p className="text-[10px] text-hint mt-0.5"> </p>
</div>
{/* 탐지 조건 */}
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-4">
<div className="text-[10px] text-muted-foreground mb-2"> </div>
<div className="grid grid-cols-3 gap-3">
<div className="bg-surface-overlay rounded-lg p-3 text-center border border-slate-700/30">
<div className="text-[9px] text-hint mb-1"></div>
<div className="text-lg font-bold text-heading"> 100m</div>
</div>
<div className="bg-surface-overlay rounded-lg p-3 text-center border border-slate-700/30">
<div className="text-[9px] text-hint mb-1"></div>
<div className="text-lg font-bold text-heading"> 30</div>
</div>
<div className="bg-surface-overlay rounded-lg p-3 text-center border border-slate-700/30">
<div className="text-[9px] text-hint mb-1"></div>
<div className="text-lg font-bold text-heading"> 3kn</div>
</div>
</div>
</CardContent>
</Card>
{/* prediction 분석 결과 기반 환적 의심 선박 */}
<RealTransshipSuspects />
</div>
);
}
// ─── 메인 페이지 ──────────────────────
export function ChinaFishing() {
const [mode, setMode] = useState<'dashboard' | 'transfer' | 'gear'>('dashboard');
const [vesselTab, setVesselTab] = useState<'특이운항' | '비허가 선박' | '제재 선박' | '관심 선박'>('특이운항');
const [statsTab, setStatsTab] = useState<'불법조업 통계' | '특이선박 통계' | '위험선박 통계'>('불법조업 통계');
// API state
const [allItems, setAllItems] = useState<VesselAnalysisItem[]>([]);
const [apiStats, setApiStats] = useState<VesselAnalysisStats | null>(null);
const [serviceAvailable, setServiceAvailable] = useState(true);
const [apiLoading, setApiLoading] = useState(false);
const [apiError, setApiError] = useState('');
const loadApi = useCallback(async () => {
setApiLoading(true);
setApiError('');
try {
const res = await fetchVesselAnalysis();
setServiceAvailable(res.serviceAvailable);
setAllItems(res.items);
setApiStats(res.stats);
} catch (e: unknown) {
setApiError(e instanceof Error ? e.message : '데이터를 불러올 수 없습니다');
setServiceAvailable(false);
} finally {
setApiLoading(false);
}
}, []);
useEffect(() => { loadApi(); }, [loadApi]);
// 중국어선 필터
const chinaVessels = useMemo(
() => allItems.filter((i) => isChinaVessel(i.mmsi)),
[allItems],
);
const chinaDark = useMemo(() => filterDarkVessels(chinaVessels), [chinaVessels]);
const chinaTransship = useMemo(() => filterTransshipSuspects(chinaVessels), [chinaVessels]);
// 센서 카운터 (API 기반)
const countersRow1 = useMemo(() => [
{ label: '통합', count: allItems.length, color: '#6b7280' },
{ label: 'AIS', count: allItems.length, color: '#3b82f6' },
{ label: 'EEZ 내', count: allItems.filter((i) => i.algorithms.location.zone !== 'EEZ_OR_BEYOND').length, color: '#8b5cf6' },
{ label: '어업선', count: allItems.filter((i) => i.classification.fishingPct > 0.5).length, color: '#10b981' },
], [allItems]);
const countersRow2 = useMemo(() => [
{ label: '중국어선', count: chinaVessels.length, color: '#f97316' },
{ label: 'Dark Vessel', count: chinaDark.length, color: '#ef4444' },
{ label: '환적 의심', count: chinaTransship.length, color: '#06b6d4' },
{ label: '고위험', count: chinaVessels.filter((i) => i.algorithms.riskScore.score >= 70).length, color: '#ef4444' },
], [chinaVessels, chinaDark, chinaTransship]);
// 특이운항 선박 리스트 (중국어선 중 riskScore >= 40)
const vesselList: VesselItem[] = useMemo(
() => chinaVessels
.filter((i) => i.algorithms.riskScore.score >= 40)
.sort((a, b) => b.algorithms.riskScore.score - a.algorithms.riskScore.score)
.slice(0, 20)
.map((item, idx) => mapToVesselItem(item, idx)),
[chinaVessels],
);
// 위험도별 분포 (도넛 차트용)
const riskDistribution = useMemo(() => {
const critical = chinaVessels.filter((i) => i.algorithms.riskScore.level === 'CRITICAL').length;
const high = chinaVessels.filter((i) => i.algorithms.riskScore.level === 'HIGH').length;
const medium = chinaVessels.filter((i) => i.algorithms.riskScore.level === 'MEDIUM').length;
const low = chinaVessels.filter((i) => i.algorithms.riskScore.level === 'LOW').length;
return { critical, high, medium, low, total: chinaVessels.length };
}, [chinaVessels]);
// 안전도 지수 계산
const safetyIndex = useMemo(() => {
if (chinaVessels.length === 0) return { risk: 0, safety: 100 };
const avgRisk = chinaVessels.reduce((s, i) => s + i.algorithms.riskScore.score, 0) / chinaVessels.length;
return { risk: Number((avgRisk / 10).toFixed(2)), safety: Number(((100 - avgRisk) / 10).toFixed(2)) };
}, [chinaVessels]);
const vesselTabs = ['특이운항', '비허가 선박', '제재 선박', '관심 선박'] as const;
const statsTabs = ['불법조업 통계', '특이선박 통계', '위험선박 통계'] as const;
const modeTabs = [
{ key: 'dashboard' as const, icon: Brain, label: 'AI 감시 대시보드' },
{ key: 'transfer' as const, icon: RefreshCw, label: '환적·접촉 탐지' },
{ key: 'gear' as const, icon: CrosshairIcon, label: '어구/어망 판별' },
];
return (
<div className="space-y-3">
{/* ── 모드 탭 (AI 대시보드 / 환적 탐지) ── */}
<div className="flex items-center gap-1 bg-surface-raised rounded-lg p-1 border border-slate-700/30 w-fit">
{modeTabs.map((tab) => (
<button
key={tab.key}
onClick={() => setMode(tab.key)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-md text-[11px] font-medium transition-colors ${
mode === tab.key
? 'bg-blue-600 text-heading'
: 'text-muted-foreground hover:text-foreground hover:bg-surface-overlay'
}`}
>
<tab.icon className="w-3.5 h-3.5" />
{tab.label}
</button>
))}
</div>
{/* 환적 탐지 모드 */}
{mode === 'transfer' && <TransferView />}
{/* 어구/어망 판별 모드 */}
{mode === 'gear' && <GearIdentification />}
{/* AI 대시보드 모드 */}
{mode === 'dashboard' && <>
{!serviceAvailable && (
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 text-yellow-400 text-xs">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>iran - </span>
</div>
)}
{apiError && <div className="text-xs text-red-400">: {apiError}</div>}
{apiLoading && (
<div className="flex items-center justify-center py-8 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin" />
</div>
)}
{/* iran 백엔드 실시간 분석 결과 */}
<RealAllVessels />
{/* ── 상단 바: 기준일 + 검색 ── */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 bg-surface-overlay rounded-lg px-3 py-1.5 border border-slate-700/40">
<Clock className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-[11px] text-label"> : {formatDateTime(new Date())}</span>
</div>
<button type="button" onClick={loadApi} className="p-1.5 rounded-lg bg-surface-overlay border border-slate-700/40 text-muted-foreground hover:text-heading transition-colors" title="새로고침">
<RotateCcw className="w-3.5 h-3.5" />
</button>
<div className="flex-1 flex items-center bg-surface-overlay border border-slate-700/40 rounded-lg px-3 py-1.5">
<Search className="w-3.5 h-3.5 text-hint mr-2" />
<input
placeholder="해역 또는 해구 번호 검색"
className="bg-transparent text-[11px] text-label placeholder:text-hint flex-1 focus:outline-none"
/>
<Search className="w-4 h-4 text-blue-500 cursor-pointer" />
</div>
</div>
{/* ── 상단 영역: 통항량 + 안전도 분석 + 관심영역 ── */}
<div className="grid grid-cols-12 gap-3">
{/* 해역별 통항량 */}
<div className="col-span-4">
<Card className="bg-surface-raised border-slate-700/30 h-full">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-bold text-heading"> </span>
{apiStats && (
<div className="flex items-center gap-2 text-[10px]">
<span className="text-hint"> </span>
<span className="text-heading font-bold font-mono">{apiStats.total.toLocaleString()}</span>
</div>
)}
</div>
<div className="flex items-center gap-2 mb-4 text-[10px] text-muted-foreground">
<span> </span>
<span className="text-lg font-extrabold text-heading">{allItems.length.toLocaleString()}</span>
<span className="text-hint">()</span>
</div>
{/* 카운터 Row 1 */}
<div className="grid grid-cols-4 gap-2 mb-2">
{countersRow1.map((c) => (
<div key={c.label} className="bg-surface-overlay rounded-lg p-2.5 text-center border border-slate-700/30">
<div className="text-[9px] text-hint mb-1">{c.label}</div>
<div className="text-lg font-extrabold text-heading font-mono">{c.count.toLocaleString()}</div>
</div>
))}
</div>
{/* 카운터 Row 2 */}
<div className="grid grid-cols-4 gap-2">
{countersRow2.map((c) => (
<div key={c.label} className="bg-surface-overlay rounded-lg p-2.5 text-center border border-slate-700/30">
<div className="text-[9px] text-hint mb-1">{c.label}</div>
<div className={`text-lg font-extrabold font-mono ${c.count > 0 ? 'text-heading' : 'text-muted'}`}>
{c.count > 0 ? c.count.toLocaleString() : '-'}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* 안전도 분석 */}
<div className="col-span-4">
<Card className="bg-surface-raised border-slate-700/30 h-full">
<CardContent className="p-4">
<span className="text-sm font-bold text-heading"> </span>
<div className="flex items-center justify-around mt-4">
<div>
<div className="text-[10px] text-muted-foreground mb-2 text-center">
<span className="text-orange-400 font-medium"></span>
</div>
<SemiGauge value={safetyIndex.risk} label="" color="#f97316" />
</div>
<div>
<div className="text-[10px] text-muted-foreground mb-2 text-center">
<span className="text-blue-400 font-medium"></span>
</div>
<SemiGauge value={safetyIndex.safety} label="" color="#3b82f6" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 관심영역 안전도 */}
<div className="col-span-4">
<Card className="bg-surface-raised border-slate-700/30 h-full">
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-heading"> </span>
<select className="bg-secondary border border-slate-700/50 rounded px-2 py-0.5 text-[10px] text-label focus:outline-none">
<option> A</option>
<option> B</option>
</select>
</div>
<p className="text-[9px] text-hint mb-3"> .</p>
<div className="flex items-center gap-4">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2 text-[11px]">
<Eye className="w-3.5 h-3.5 text-blue-400" />
<span className="text-muted-foreground"></span>
<span className="text-green-400 font-bold ml-auto"></span>
</div>
<div className="flex items-center gap-2 text-[11px]">
<AlertTriangle className="w-3.5 h-3.5 text-red-400" />
<span className="text-muted-foreground"></span>
<span className="text-green-400 font-bold ml-auto"></span>
</div>
<div className="flex items-center gap-2 text-[11px]">
<Radio className="w-3.5 h-3.5 text-purple-400" />
<span className="text-muted-foreground"></span>
<span className="text-green-400 font-bold ml-auto"></span>
</div>
</div>
<CircleGauge value={chinaVessels.length > 0 ? Number(((1 - riskDistribution.critical / Math.max(chinaVessels.length, 1)) * 100).toFixed(1)) : 100} label="" />
</div>
</CardContent>
</Card>
</div>
</div>
{/* ── 하단 영역: 선박 리스트 + 통계 ── */}
<div className="grid grid-cols-12 gap-3">
{/* 좌: 선박 리스트 (탭) */}
<div className="col-span-5">
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-0">
{/* 탭 헤더 */}
<div className="flex border-b border-slate-700/30">
{vesselTabs.map((tab) => (
<button
key={tab}
onClick={() => setVesselTab(tab)}
className={`flex-1 py-2.5 text-[11px] font-medium transition-colors ${
vesselTab === tab
? 'text-heading border-b-2 border-blue-500 bg-surface-overlay'
: 'text-hint hover:text-label'
}`}
>
{tab}
</button>
))}
</div>
{/* 선박 목록 */}
<div className="max-h-[420px] overflow-y-auto">
{vesselList.length === 0 && (
<div className="px-4 py-8 text-center text-hint text-xs">
{apiLoading ? '데이터 로딩 중...' : '중국어선 특이운항 데이터가 없습니다'}
</div>
)}
{vesselList.map((v) => (
<div
key={v.id}
className="flex items-center gap-3 px-4 py-3 border-b border-slate-700/20 hover:bg-surface-overlay transition-colors cursor-pointer group"
>
<StatusRing status={v.status} riskPct={v.riskPct} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 text-[10px] text-hint mb-0.5">
<span>MMSI | <span className="text-label">{v.mmsi}</span></span>
<span> | <span className="text-label">{v.source}</span></span>
</div>
<div className="flex items-center gap-2">
<span className="text-[12px] font-bold text-heading">{v.name}</span>
<Badge className="bg-blue-500/20 text-blue-400 border-0 text-[8px] px-1.5 py-0">{v.type}</Badge>
</div>
<div className="flex items-center gap-1 mt-0.5 text-[9px] text-hint">
<span>{v.country}</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-hint group-hover:text-muted-foreground shrink-0" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* 우: 통계 + 하단 카드 3개 */}
<div className="col-span-7 space-y-3">
{/* 통계 차트 */}
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-0">
{/* 탭 */}
<div className="flex border-b border-slate-700/30">
{statsTabs.map((tab) => (
<button
key={tab}
onClick={() => setStatsTab(tab)}
className={`flex-1 py-2.5 text-[11px] font-medium transition-colors ${
statsTab === tab
? 'text-heading border-b-2 border-green-500 bg-surface-overlay'
: 'text-hint hover:text-label'
}`}
>
{tab}
</button>
))}
</div>
<div className="p-4 flex gap-4">
{/* 월별 통계 - API 미지원, 준비중 안내 */}
<div className="flex-1 flex flex-col items-center justify-center py-8">
<div className="text-muted-foreground text-xs mb-2"> </div>
<div className="text-hint text-[10px] bg-surface-overlay rounded-lg px-4 py-3 border border-border">
API . .
</div>
</div>
{/* 위험도 분포 도넛 */}
<div className="flex flex-col items-center justify-center gap-3 w-28">
<div className="relative w-[80px] h-[80px]">
<EcPieChart
data={[
{ name: 'CRITICAL', value: riskDistribution.critical || 1, color: '#ef4444' },
{ name: 'HIGH', value: riskDistribution.high || 1, color: '#f97316' },
{ name: 'MEDIUM', value: riskDistribution.medium || 1, color: '#eab308' },
{ name: 'LOW', value: riskDistribution.low || 1, color: '#3b82f6' },
]}
height={80}
innerRadius={24}
outerRadius={34}
/>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className="text-sm font-extrabold text-heading">{riskDistribution.total}</span>
<span className="text-[7px] text-hint"></span>
</div>
</div>
<div className="space-y-0.5 text-[8px]">
<div className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-red-500" /><span className="text-hint">CRITICAL {riskDistribution.critical}</span></div>
<div className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-orange-500" /><span className="text-hint">HIGH {riskDistribution.high}</span></div>
<div className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-yellow-500" /><span className="text-hint">MEDIUM {riskDistribution.medium}</span></div>
<div className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-blue-500" /><span className="text-hint">LOW {riskDistribution.low}</span></div>
</div>
</div>
</div>
{/* 다운로드 버튼 */}
<div className="px-4 pb-3 flex justify-end">
<button type="button" className="px-3 py-1 bg-secondary border border-slate-700/50 rounded text-[10px] text-label hover:bg-switch-background transition-colors">
</button>
</div>
</CardContent>
</Card>
{/* 하단 카드 3개 */}
<div className="grid grid-cols-3 gap-3">
{/* 최근 위성영상 분석 */}
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-heading"> </span>
<button className="text-[9px] text-blue-400 hover:underline"> </button>
</div>
<div className="space-y-1.5 text-[10px]">
<div className="flex gap-2">
<span className="text-hint shrink-0">VIIRS</span>
<span className="text-label">| 2023-08-11 02:00:00</span>
</div>
<div className="flex gap-2">
<span className="text-hint shrink-0"></span>
<span className="text-label truncate">| BSG-117-20230194-orho.tif</span>
</div>
<div className="flex gap-2">
<span className="text-hint shrink-0">CSV</span>
<span className="text-label truncate">| 2023.07.17_ship_dection.clustog.csv</span>
</div>
</div>
</CardContent>
</Card>
{/* 기상 예보 */}
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-heading"> </span>
<button className="text-[9px] text-blue-400 hover:underline"> </button>
</div>
<div className="flex items-center gap-3">
<div className="text-center">
<Cloud className="w-8 h-8 text-yellow-400 mx-auto" />
</div>
<div>
<div className="text-[9px] text-muted-foreground"></div>
<div className="flex items-baseline gap-1">
<span className="text-2xl font-extrabold text-heading">28.1</span>
<span className="text-sm text-muted-foreground">°C</span>
</div>
<div className="text-[9px] text-hint"> ~ 🌊 0.5~0.5m</div>
</div>
</div>
</CardContent>
</Card>
{/* VTS연계 현황 */}
<Card className="bg-surface-raised border-slate-700/30">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-heading">VTS연계 </span>
<button className="text-[9px] text-blue-400 hover:underline"> </button>
</div>
<div className="grid grid-cols-2 gap-1.5">
{VTS_ITEMS.map((vts) => (
<div
key={vts.name}
className={`flex items-center gap-1.5 px-2 py-1 rounded text-[10px] ${
vts.active
? 'bg-orange-500/15 text-orange-400 border border-orange-500/20'
: 'bg-surface-overlay text-muted-foreground border border-slate-700/30'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${vts.active ? 'bg-orange-400' : 'bg-muted'}`} />
{vts.name}
</div>
))}
</div>
<div className="flex justify-between mt-2">
<button className="text-hint hover:text-heading transition-colors">
<ChevronLeft className="w-4 h-4" />
</button>
<button className="text-hint hover:text-heading transition-colors">
<ChevronRight className="w-4 h-4" />
</button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</>}
</div>
);
}