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 (
{/* 배경 호 */} {/* 값 호 */}
{value.toFixed(2)} %
{label}
); } function CircleGauge({ value, label }: { value: number; label: string }) { const circumference = 2 * Math.PI * 42; const offset = circumference - (value / 100) * circumference; return (
{value} %
{label}
); } function StatusRing({ status, riskPct }: { status: VesselStatus; riskPct: number }) { const colors: Record = { '의심': { 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 (
{status} {riskPct}%
); } // ─── 메인 페이지 ────────────────────── // ─── 환적 탐지 뷰 ───────────────────── function TransferView() { return (

환적·접촉 탐지

선박 간 근접 접촉 및 환적 의심 행위 분석

{/* 탐지 조건 */}
탐지 조건
거리
≤ 100m
시간
≥ 30분
속도
≤ 3kn
{/* prediction 분석 결과 기반 환적 의심 선박 */}
); } // ─── 메인 페이지 ────────────────────── 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([]); const [apiStats, setApiStats] = useState(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 (
{/* ── 모드 탭 (AI 대시보드 / 환적 탐지) ── */}
{modeTabs.map((tab) => ( ))}
{/* 환적 탐지 모드 */} {mode === 'transfer' && } {/* 어구/어망 판별 모드 */} {mode === 'gear' && } {/* AI 대시보드 모드 */} {mode === 'dashboard' && <> {!serviceAvailable && (
iran 분석 서비스 미연결 - 실시간 데이터를 불러올 수 없습니다
)} {apiError &&
에러: {apiError}
} {apiLoading && (
)} {/* iran 백엔드 실시간 분석 결과 */} {/* ── 상단 바: 기준일 + 검색 ── */}
기준 : {formatDateTime(new Date())}
{/* ── 상단 영역: 통항량 + 안전도 분석 + 관심영역 ── */}
{/* 해역별 통항량 */}
해역별 통항량 {apiStats && (
분석 대상 {apiStats.total.toLocaleString()}척
)}
해역 전체 통항량 {allItems.length.toLocaleString()} (척)
{/* 카운터 Row 1 */}
{countersRow1.map((c) => (
{c.label}
{c.count.toLocaleString()}
))}
{/* 카운터 Row 2 */}
{countersRow2.map((c) => (
{c.label}
0 ? 'text-heading' : 'text-muted'}`}> {c.count > 0 ? c.count.toLocaleString() : '-'}
))}
{/* 안전도 분석 */}
안전도 분석
종합 위험지수
종합 안전지수
{/* 관심영역 안전도 */}
관심영역 안전도

설정한 관심 영역을 선택후 조회를 눌러주세요.

특이운항 정상
불법조업 정상
비허가 정상
0 ? Number(((1 - riskDistribution.critical / Math.max(chinaVessels.length, 1)) * 100).toFixed(1)) : 100} label="" />
{/* ── 하단 영역: 선박 리스트 + 통계 ── */}
{/* 좌: 선박 리스트 (탭) */}
{/* 탭 헤더 */}
{vesselTabs.map((tab) => ( ))}
{/* 선박 목록 */}
{vesselList.length === 0 && (
{apiLoading ? '데이터 로딩 중...' : '중국어선 특이운항 데이터가 없습니다'}
)} {vesselList.map((v) => (
MMSI | {v.mmsi} 출처 | {v.source}
{v.name} {v.type}
{v.country}
))}
{/* 우: 통계 + 하단 카드 3개 */}
{/* 통계 차트 */} {/* 탭 */}
{statsTabs.map((tab) => ( ))}
{/* 월별 통계 - API 미지원, 준비중 안내 */}
월별 불법조업 통계
월별 집계 API 연동 준비중입니다. 실시간 위험도 분포는 우측 도넛을 참고하세요.
{/* 위험도 분포 도넛 */}
{riskDistribution.total} 중국어선
CRITICAL {riskDistribution.critical}
HIGH {riskDistribution.high}
MEDIUM {riskDistribution.medium}
LOW {riskDistribution.low}
{/* 다운로드 버튼 */}
{/* 하단 카드 3개 */}
{/* 최근 위성영상 분석 */}
최근 위성영상 분석
VIIRS | 2023-08-11 02:00:00
이미지 | BSG-117-20230194-orho.tif
CSV | 2023.07.17_ship_dection.clustog.csv
{/* 기상 예보 */}
기상 예보
전남서부남해앞바다
28.1 °C
흐림 남~남서 🌊 0.5~0.5m
{/* VTS연계 현황 */}
VTS연계 현황
{VTS_ITEMS.map((vts) => (
{vts.name}
))}
}
); }