import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Search, Anchor, Ship, Eye, AlertTriangle, CheckCircle, XCircle, ChevronRight, ChevronDown, Info, Shield, Radar, Target, Waves, ArrowRight, Flag, Zap, HelpCircle } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; // ─── 판별 기준 데이터 ───────────────── type GearType = 'trawl' | 'gillnet' | 'purseSeine' | 'setNet' | 'trap' | 'unknown'; type Origin = 'china' | 'korea' | 'uncertain'; type Confidence = 'high' | 'medium' | 'low'; interface IdentificationResult { origin: Origin; confidence: Confidence; gearType: GearType; gearSubType: string; gbCode: string; koreaName: string; reasons: string[]; warnings: string[]; actionRequired: string; alertLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; } // ─── 판별 입력 폼 상태 ──────────────── interface GearInput { // 어구 물리적 특성 gearCategory: GearType; meshSize: number | null; // mm netLength: number | null; // m netWidth: number | null; // m hasChineseMarkings: boolean; hasBuoys: boolean; buoyCount: number | null; hasFrame: boolean; // 프레임 구조 (정치망) hasDrawstring: boolean; // 죔줄 (선망) // AIS/선박 정보 aisActive: boolean; aisGapHours: number | null; mmsiPrefix: string; permitCode: string; // C21, C22, C23, C25, C40 등 vesselTonnage: number | null; vesselCount: number; hasCompanionVessel: boolean; // 부속선 동반 // 행동 패턴 speedKnots: number | null; trajectoryPattern: 'lawnmowing' | 'stationary' | 'circular' | 'drifting' | 'linear' | 'unknown'; operatingHours: number | null; isNightOperation: boolean; hasFishLight: boolean; // 집어등 // 위치/시기 seaArea: string; // 수역 I~IV discoveryDate: string; hasVesselNearby: boolean; } const DEFAULT_INPUT: GearInput = { gearCategory: 'unknown', meshSize: null, netLength: null, netWidth: null, hasChineseMarkings: false, hasBuoys: false, buoyCount: null, hasFrame: false, hasDrawstring: false, aisActive: true, aisGapHours: null, mmsiPrefix: '', permitCode: '', vesselTonnage: null, vesselCount: 1, hasCompanionVessel: false, speedKnots: null, trajectoryPattern: 'unknown', operatingHours: null, isNightOperation: false, hasFishLight: false, seaArea: '', discoveryDate: '', hasVesselNearby: true, }; // ─── 판별 엔진 ──────────────────────── function identifyGear(input: GearInput): IdentificationResult { const reasons: string[] = []; const warnings: string[] = []; let chinaScore = 0; let koreaScore = 0; let gearType: GearType = input.gearCategory; let gearSubType = ''; let gbCode = ''; let koreaName = ''; let alertLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' = 'LOW'; // ── 1단계: 허가코드 기반 즉시 판별 ── const permitPrefix = input.permitCode.toUpperCase(); if (permitPrefix.startsWith('C21')) { chinaScore += 50; gearType = 'trawl'; gearSubType = '2척식저인망(PT) 본선/부속선'; gbCode = 'TDS (쌍선저층트롤)'; koreaName = '쌍끌이기선저인망'; reasons.push('허가코드 C21-xxxxx → 중국 2척식저인망(PT) 확정'); } else if (permitPrefix.startsWith('C22')) { chinaScore += 50; gearType = 'trawl'; gearSubType = '1척식저인망(OT)'; gbCode = 'TDD (단선저층트롤)'; koreaName = '기선저인망'; reasons.push('허가코드 C22-xxxxx → 중국 1척식저인망(OT) 확정'); } else if (permitPrefix.startsWith('C23')) { chinaScore += 50; gearType = 'purseSeine'; gearSubType = '위망(PS) — 宁波海裕 선단'; gbCode = 'WDD (집어등단선선망)'; koreaName = '대형선망/근해선망'; reasons.push('허가코드 C23-xxxxx → 중국 위망(PS), 宁波海裕渔业 소속 확정'); warnings.push('위망 16척은 연중 전수역 허가 — 시기/장소 기반 자동 위반 판별 불가'); } else if (permitPrefix.startsWith('C25')) { chinaScore += 50; gearType = 'gillnet'; gearSubType = '유망(GN) — 유자망'; gbCode = 'CLD (단선유자망)'; koreaName = '유자망/연안유자망'; reasons.push('허가코드 C25-xxxxx → 중국 유망(GN) 확정'); warnings.push('유망은 AIS 차단 최다 업종 — 다크베셀 주의'); } else if (permitPrefix.startsWith('C40')) { chinaScore += 50; gearSubType = '운반선(FC)'; gbCode = '해당없음 (운반 전용)'; koreaName = '운반선'; reasons.push('허가코드 C40-xxxxx → 중국 운반선(FC) 확정, 할당량 0톤'); warnings.push('조업선 0.5NM 이내 접근 시 환적 의심 알람 발동'); } // ── 2단계: 중국어 표기 확인 ── if (input.hasChineseMarkings) { chinaScore += 30; reasons.push('어구/부표에 중국어 표기 확인 → 중국 어구 강력 추정'); } // ── 3단계: 어구 유형별 물리적 특성 판별 ── // ■ 트롤(저인망) 판별 if (gearType === 'trawl' || input.trajectoryPattern === 'lawnmowing') { if (!gearType || gearType === 'unknown') gearType = 'trawl'; // 속도 패턴 if (input.speedKnots !== null && input.speedKnots >= 2 && input.speedKnots <= 5) { reasons.push(`속도 ${input.speedKnots}kt → 트롤 조업 속도 범위(2~5kt) 일치`); } // 쌍선 운용 여부 if (input.hasCompanionVessel || input.vesselCount === 2) { chinaScore += 15; gearSubType = gearSubType || '2척식저인망(PT) 추정'; gbCode = gbCode || 'TDS'; reasons.push('2척 편대 운용 확인 → 중국 PT(쌍선저인망) 구조와 일치'); if (input.vesselTonnage !== null) { if (input.vesselTonnage >= 68 && input.vesselTonnage <= 297) { chinaScore += 10; reasons.push(`선박 톤수 ${input.vesselTonnage}톤 → 중국 PT 평균 범위(68~297톤) 일치`); } } } else if (input.vesselCount === 1 && input.vesselTonnage !== null && input.vesselTonnage >= 180) { chinaScore += 8; gearSubType = gearSubType || '1척식저인망(OT) 추정'; gbCode = gbCode || 'TDD'; reasons.push(`단선 운용 + ${input.vesselTonnage}톤 → 중국 OT(1척식저인망) 추정`); } // 망목 규격 if (input.meshSize !== null) { if (input.meshSize < 54) { chinaScore += 10; warnings.push(`망목 ${input.meshSize}mm — 한국 기준 54mm 미만 → 금지 어구 사용 위반`); alertLevel = 'HIGH'; } else { reasons.push(`망목 ${input.meshSize}mm — 한국 기준(54mm) 충족`); } } koreaName = koreaName || (input.vesselCount >= 2 ? '쌍끌이기선저인망' : '기선저인망'); } // ■ 자망(유자망) 판별 if (gearType === 'gillnet' || input.trajectoryPattern === 'stationary' || input.trajectoryPattern === 'drifting') { if (!gearType || gearType === 'unknown') gearType = 'gillnet'; // AIS 소실 — 중국 자망의 핵심 지표 if (!input.aisActive || (input.aisGapHours !== null && input.aisGapHours >= 6)) { chinaScore += 25; reasons.push(`AIS ${input.aisGapHours || '?'}시간 소실 → 중국 유망(GN) 다크베셀 전술 패턴`); warnings.push('자망 탐지 핵심: AIS 공백 구간을 SAR 영상과 교차 검증 필수'); alertLevel = alertLevel === 'LOW' ? 'HIGH' : alertLevel; } else if (input.aisActive) { koreaScore += 10; reasons.push('AIS 지속 송출 → 한국 유자망어선은 V-PASS + AIS 이중 추적 의무'); } // 그물 규모 if (input.netLength !== null && input.netLength > 500) { chinaScore += 15; reasons.push(`그물 길이 ${input.netLength}m → 중국식 광폭·장형 대형 자망 특성`); } else if (input.netLength !== null && input.netLength <= 500) { koreaScore += 5; reasons.push(`그물 길이 ${input.netLength}m → 한국 규격 범위 내`); } // 망목 if (input.meshSize !== null) { if (input.meshSize >= 50 && input.meshSize <= 100) { reasons.push(`망목 ${input.meshSize}mm — 한국 유자망 기준(50~100mm) 범위 내`); } else if (input.meshSize < 50) { chinaScore += 10; warnings.push(`망목 ${input.meshSize}mm — 한국 기준 미달, 불법 어구 의심`); } } // 정지 패턴 if (input.speedKnots !== null && input.speedKnots < 2) { reasons.push(`속도 ${input.speedKnots}kt → 자망 조업 속도(0~2kt) 범위 일치`); } // 부표 다수 발견 if (input.hasBuoys && input.buoyCount !== null && input.buoyCount >= 10) { reasons.push(`부표 ${input.buoyCount}개 발견 → SAR 고해상도에서 자망 설치 식별 지표`); } gbCode = gbCode || 'CLD/CLS'; koreaName = koreaName || '유자망어업'; gearSubType = gearSubType || '유자망(Gillnet)'; } // ■ 선망(위망) 판별 if (gearType === 'purseSeine' || input.trajectoryPattern === 'circular') { if (!gearType || gearType === 'unknown') gearType = 'purseSeine'; // 원형 궤적 + 고→저속 급변 if (input.trajectoryPattern === 'circular') { reasons.push('원형/타원형 궤적 탐지 → 선망(Purse Seine) 패턴 확정 (신뢰도 94~97%)'); } if (input.speedKnots !== null && input.speedKnots >= 8) { reasons.push(`고속 ${input.speedKnots}kt 원형 이동 → 선망 포획 단계(8~10kt) 일치`); } // 선단 구조 — 중국 위망 핵심 if (input.vesselCount >= 3) { chinaScore += 25; reasons.push(`${input.vesselCount}척 클러스터 → 중국 위망 선단(모선+운반선+조명선) 구조`); gearSubType = gearSubType || '위망 선단(Fleet)'; } else if (input.vesselCount <= 2) { koreaScore += 10; reasons.push('소규모 선박 구성 → 한국 선망어업 가능성'); } // 야간 집어등 if (input.hasFishLight || input.isNightOperation) { chinaScore += 20; reasons.push('야간 집어등(灯光) 사용 → 중국 WDD(집어등선망) 코드, 위반 건수 1위'); gbCode = gbCode || 'WDD'; warnings.push('야간 EO 위성에서 최우선 탐지 대상'); } // 초대형 선박 if (input.vesselTonnage !== null && input.vesselTonnage >= 400) { chinaScore += 15; reasons.push(`${input.vesselTonnage}톤 → 宁波海裕 초대형 모선급(宁渔22·23, 490~541톤) 의심`); warnings.push('어획 할당량 0톤 등록 선박 — 해상 냉동·집하 기지 역할 확인 필요'); } gbCode = gbCode || 'WDD/WDW'; koreaName = koreaName || '선망어업'; gearSubType = gearSubType || '선망(Purse Seine)'; if (input.hasDrawstring) { reasons.push('죔줄(Purseline) 구조 확인 → 선망 어구 확정'); } } // ■ 정치망 판별 if (gearType === 'setNet' || input.hasFrame) { gearType = 'setNet'; chinaScore += 30; // 한중 EEZ 내 정치망은 중국 미허가 gbCode = 'ZD/ZS (張網)'; koreaName = '연안정치망/대형정치망'; gearSubType = '정치망(張網) — 단묘/복묘장망'; alertLevel = 'CRITICAL'; reasons.push('정치망(張網) 구조 확인 → 한중어업협정 특정어업수역 내 중국 미허가 어구'); warnings.push('정치망은 EEZ 내 중국어선 사용 절대 금지 — 발견 즉시 수거 및 CRITICAL 처리'); if (!input.hasVesselNearby) { reasons.push('선박 없이 어구만 발견 → 중국어선 투기 정치망 전형적 패턴(이어도·서남해 다수)'); } } // ■ 통발 판별 if (gearType === 'trap') { gbCode = 'L (笼壶)'; koreaName = '근해통발/연안통발'; gearSubType = '통발(笼壶)'; reasons.push('통발 형태 어구 — 한중어업협정 비포함 어구, 적발 사례 드묾'); } // ── 4단계: 위치/시기 기반 보정 ── if (input.seaArea) { const area = input.seaArea.toUpperCase(); // 수역 IV에 PT/OT 선박이면 수역 이탈 if ((area.includes('IV') || area.includes('4') || area.includes('서해')) && (permitPrefix.startsWith('C21') || permitPrefix.startsWith('C22'))) { warnings.push('수역Ⅳ(서해)에서 저인망(PT/OT) 탐지 → 허가 수역 이탈 위반'); alertLevel = 'CRITICAL'; } // 수역 I에 GN 선박이면 수역 이탈 if ((area.includes('I') || area.includes('1') || area.includes('동해')) && area.indexOf('V') === -1 && permitPrefix.startsWith('C25')) { warnings.push('수역Ⅰ(동해)에서 유망(GN) 탐지 → 허가 수역 이탈 위반'); alertLevel = 'CRITICAL'; } } // 날짜 기반 휴어기 판별 if (input.discoveryDate) { const date = new Date(input.discoveryDate); const month = date.getMonth() + 1; const day = date.getDate(); // PT/OT 휴어기: 4/16 ~ 10/15 if (permitPrefix.startsWith('C21') || permitPrefix.startsWith('C22')) { if ((month === 4 && day >= 16) || (month > 4 && month < 10) || (month === 10 && day <= 15)) { warnings.push(`${month}월 ${day}일 — 저인망(PT/OT) 휴어기(4/16~10/15) 위반`); alertLevel = 'CRITICAL'; } } // GN 휴어기: 6/2 ~ 8/31 if (permitPrefix.startsWith('C25')) { if ((month === 6 && day >= 2) || (month === 7) || (month === 8)) { warnings.push(`${month}월 ${day}일 — 유망(GN) 휴어기(6/2~8/31) 위반`); alertLevel = 'CRITICAL'; } } // 7~8월: PS 16척 외 모든 중국어선 비허가 if (month === 7 || month === 8) { if (!permitPrefix.startsWith('C23') && !permitPrefix.startsWith('C40') && chinaScore > 20) { warnings.push('7~8월 최대 감시 기간 — 위망(PS) 16척 외 전 중국어선 비허가'); } } } // ── 5단계: 최종 판정 ── let origin: Origin; let confidence: Confidence; if (chinaScore >= 50) { origin = 'china'; confidence = chinaScore >= 80 ? 'high' : chinaScore >= 60 ? 'medium' : 'low'; } else if (koreaScore >= 30 && chinaScore < 20) { origin = 'korea'; confidence = koreaScore >= 50 ? 'high' : 'medium'; } else if (chinaScore > koreaScore) { origin = 'china'; confidence = 'low'; } else if (koreaScore > chinaScore) { origin = 'korea'; confidence = 'low'; } else { origin = 'uncertain'; confidence = 'low'; reasons.push('판별 근거 부족 — 추가 현장 확인 필요 (SAR/RF 교차 검증 권고)'); } // 알람 등급 보정 if (origin === 'china' && alertLevel === 'LOW') { alertLevel = gearType === 'setNet' ? 'CRITICAL' : 'MEDIUM'; } const actionMap: Record = { 'china-CRITICAL': '즉시 나포 대상 — 정선 명령 후 승선 검사 실시', 'china-HIGH': '우선 추적 — SAR/RF 교차 검증 후 임검 준비', 'china-MEDIUM': '지속 감시 — 허가증 확인 대상 선정', 'china-LOW': '일반 감시 — 추가 정보 수집 후 재판별', 'korea-CRITICAL': '한국 어선이나 금지 어구 사용 — 관할 해경서 통보', 'korea-HIGH': '한국 어선 확인 — 허가 조건 이행 점검', 'korea-MEDIUM': '한국 어선 추정 — 일반 관리', 'korea-LOW': '추가 확인 필요', 'uncertain-LOW': '판별 불가 — 현장 승선 검사 또는 SAR·RF 교차 확인 필수', }; return { origin, confidence, gearType: gearType || 'unknown', gearSubType: gearSubType || '미분류', gbCode: gbCode || '미확인', koreaName: koreaName || '미확인', reasons, warnings, actionRequired: actionMap[`${origin}-${alertLevel}`] || actionMap[`${origin}-LOW`] || '추가 확인 필요', alertLevel, }; } // ─── 서브 컴포넌트 ──────────────────── function SectionHeader({ icon: Icon, title, color }: { icon: React.ElementType; title: string; color: string }) { return (

{title}

); } function FormField({ label, children, hint }: { label: string; children: React.ReactNode; hint?: string }) { return (
{children} {hint &&

{hint}

}
); } function InputField({ value, onChange, placeholder, type = 'text', className = '', label }: { value: string | number | null; onChange: (v: string) => void; placeholder: string; type?: string; className?: string; label?: string; }) { return ( onChange(e.target.value)} placeholder={placeholder} className={`w-full bg-surface-overlay border border-slate-700/50 rounded-md px-2.5 py-1.5 text-xs text-heading placeholder:text-hint focus:border-blue-500/50 focus:outline-none ${className}`} /> ); } function Toggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string }) { return (