// ═══ 중국 어선 조업 분석 — GC-KCG-2026-001 보고서 기반 ═══ // 한중어업협정 허가현황 (2026.01.06, 906척) + GB/T 5147-2003 어구 분류 import type { Ship } from '../types'; import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; import { point } from '@turf/helpers'; import type { Feature, MultiPolygon } from 'geojson'; import fishingZonesWgs84 from '../data/zones/fishing-zones-wgs84.json'; /** * 중국 허가 업종 코드 (허가번호 접두사) * PT(C21): 2척식저인망 본선 323척 + 부속선(PT-S) 323척 * OT(C22): 1척식저인망 13척 * PS(C23): 위망(선망) 16척 (宁波海裕 단일법인) * GN(C25): 유망(유자망) 200척 * FC: 운반선 31척 */ export type FishingGearType = 'trawl_pair' | 'trawl_single' | 'gillnet' | 'stow_net' | 'purse_seine' | 'carrier' | 'unknown'; export interface FishingAnalysis { gearType: FishingGearType; gearTypeKo: string; permitCode: string; // PT, OT, GN, PS, FC gbCode: string; // GB/T 5147-2003 코드 isOperating: boolean; operatingStatusKo: string; confidence: number; riskLevel: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; riskReason: string; } /** 업종별 메타데이터 */ const GEAR_META: Record = { trawl_pair: { ko: '2척식 저인망(PT)', icon: '🔻', color: '#ef4444', permitCode: 'PT', gbCode: 'TDS', speedRange: [2, 5], aiConfidence: '89~95%' }, trawl_single: { ko: '1척식 저인망(OT)', icon: '🔻', color: '#dc2626', permitCode: 'OT', gbCode: 'TDD', speedRange: [2, 5], aiConfidence: '89~92%' }, gillnet: { ko: '유자망(GN)', icon: '🔲', color: '#f97316', permitCode: 'GN', gbCode: 'CLD', speedRange: [0, 2], aiConfidence: '74~80%' }, stow_net: { ko: '안강망(Z)', icon: '🪤', color: '#eab308', permitCode: '-', gbCode: 'ZD', speedRange: [0, 1], aiConfidence: '70~78%' }, purse_seine: { ko: '위망/선망(PS)', icon: '🔄', color: '#3b82f6', permitCode: 'PS', gbCode: 'WDD', speedRange: [3, 10], aiConfidence: '94~97%' }, carrier: { ko: '운반선(FC)', icon: '🚢', color: '#6b7280', permitCode: 'FC', gbCode: '-', speedRange: [0, 12], aiConfidence: '-' }, unknown: { ko: '미분류', icon: '🐟', color: '#9ca3af', permitCode: '-', gbCode: '-', speedRange: [0, 0], aiConfidence: '-' }, }; export { GEAR_META as GEAR_LABELS }; export type FishingZoneId = 'ZONE_I' | 'ZONE_II' | 'ZONE_III' | 'ZONE_IV' | 'OUTSIDE'; export interface FishingZoneInfo { zone: FishingZoneId; name: string; allowed: string[]; } /** 수역별 허가 업종 */ const ZONE_ALLOWED: Record = { ZONE_I: ['PS', 'FC'], ZONE_II: ['PT', 'OT', 'GN', 'PS', 'FC'], ZONE_III: ['PT', 'OT', 'GN', 'PS', 'FC'], ZONE_IV: ['GN', 'PS', 'FC'], }; /** * 특정어업수역 Ⅰ~Ⅳ 폴리곤 (사전 변환된 WGS84 GeoJSON) */ export const ZONE_POLYGONS = fishingZonesWgs84.features.map(f => ({ id: f.properties.id as FishingZoneId, name: f.properties.name, allowed: ZONE_ALLOWED[f.properties.id] ?? [], geojson: f as unknown as Feature, })); /** * 특정어업수역 폴리곤 기반 수역 분류 */ export function classifyFishingZone(lat: number, lng: number): FishingZoneInfo { const pt = point([lng, lat]); for (const z of ZONE_POLYGONS) { if (booleanPointInPolygon(pt, z.geojson)) { return { zone: z.id, name: z.name, allowed: z.allowed }; } } return { zone: 'OUTSIDE', name: '수역 외', allowed: [] }; } /** * 업종별 허가 기간 (월/일) */ const PERMIT_PERIODS: Record = { PT: { periods: [[1,1, 4,15], [10,16, 12,31]], label: '1/1~4/15, 10/16~12/31' }, OT: { periods: [[1,1, 4,15], [10,16, 12,31]], label: '1/1~4/15, 10/16~12/31' }, GN: { periods: [[2,1, 6,1], [9,1, 12,31]], label: '2/1~6/1, 9/1~12/31' }, PS: { periods: [[1,1, 12,31]], label: '연중' }, FC: { periods: [[1,1, 12,31]], label: '연중' }, }; function isInPermitPeriod(permitCode: string, date: Date): boolean { const pp = PERMIT_PERIODS[permitCode]; if (!pp) return false; const m = date.getMonth() + 1; const d = date.getDate(); const dayOfYear = m * 100 + d; return pp.periods.some(([m1, d1, m2, d2]) => dayOfYear >= m1 * 100 + d1 && dayOfYear <= m2 * 100 + d2); } /** * AIS 신호 기반 중국 어선 조업 분석 (보고서 4장 기반) * * 트롤(PT/OT): 2~5kn, Lawn-mowing 지그재그, 방향변화 <30° * 자망(GN): 0~2kn 정지·재방문, AIS OFF 빈번 * 선망(PS): 8~10kn→3kn 급전환, 원형 궤적 * 운반선(FC): 환적 패턴 (0.5NM 접근 + 2kn이하 + 30분) */ export function analyzeFishing(ship: Ship): FishingAnalysis { const speed = ship.speed; const status = ship.status?.toLowerCase() || ''; const isAnchored = status.includes('anchor') || status.includes('moor'); // 방향 변화율 (trail 기반) let headingVariance = 0; if (ship.trail && ship.trail.length >= 3) { const pts = ship.trail.slice(-6); const headings: number[] = []; for (let i = 1; i < pts.length; i++) { const dlng = pts[i][1] - pts[i - 1][1]; const dlat = pts[i][0] - pts[i - 1][0]; headings.push(Math.atan2(dlng, dlat) * 180 / Math.PI); } if (headings.length >= 2) { const diffs = headings.slice(1).map((h, i) => { let d = Math.abs(h - headings[i]); if (d > 180) d = 360 - d; return d; }); headingVariance = diffs.reduce((a, b) => a + b, 0) / diffs.length; } } let gearType: FishingGearType = 'unknown'; let isOperating = false; let operatingStatusKo = '이동 중'; let confidence = 0.3; // === 패턴 매칭 (보고서 4.1~4.3 기준) === if (isAnchored || speed > 12) { // 정박 또는 고속 이동 → 비조업 gearType = 'unknown'; isOperating = false; operatingStatusKo = isAnchored ? '정박 중' : '고속 이동'; confidence = 0.8; } else if (speed >= 8 && speed <= 10 && headingVariance > 30) { // 선망(PS) 포위 단계: 8~10kn + 원형 궤적 gearType = 'purse_seine'; isOperating = true; operatingStatusKo = '선망 포위 중 (8~10kn 원형)'; confidence = 0.94; } else if (speed <= 3 && headingVariance > 40) { // 선망(PS) 죔줄 단계: 3kn 이하 + 이전 고속→저속 급전환 gearType = 'purse_seine'; isOperating = true; operatingStatusKo = '선망 죔줄 조임 중 (<3kn)'; confidence = 0.85; } else if (speed >= 2 && speed <= 5 && headingVariance < 30) { // 트롤(PT/OT): 2~5kn + 완만한 방향 (Lawn-mowing) gearType = headingVariance < 15 ? 'trawl_pair' : 'trawl_single'; isOperating = true; operatingStatusKo = `트롤 예인 중 (${speed.toFixed(1)}kn 지그재그)`; confidence = headingVariance < 15 ? 0.92 : 0.89; } else if (speed < 2) { // 0~2kn 극저속 → 조업 중 or 정박 대기 if (isAnchored || speed < 0.3) { // 완전 정지/정박 → 비조업 (대기 중) gearType = 'unknown'; isOperating = false; operatingStatusKo = speed < 0.3 ? '정지/대기 중' : '정박 중'; confidence = 0.7; } else if (speed >= 0.3 && speed < 1 && headingVariance < 8) { // 극저속 + 방향 변화 거의 없음 → 안강망 (조류 이용 수동 어구) gearType = 'stow_net'; isOperating = true; operatingStatusKo = '안강망 조업 추정 (조류 이용)'; confidence = 0.65; } else if (speed >= 0.3 && speed < 2) { // 저속 이동 → 자망 투망/양망 gearType = 'gillnet'; isOperating = true; operatingStatusKo = speed < 1 ? '자망 투하/대기 중' : '자망 양망 중 (극저속)'; confidence = 0.72; } } else if (speed >= 5 && speed < 8) { // 중속 이동 → 조업지 이동 또는 운반선 gearType = 'carrier'; isOperating = false; operatingStatusKo = '이동 중 (조업지 이동 추정)'; confidence = 0.5; } // === 위반 위험도 판별 (보고서 5장 기반) === const now = new Date(); const meta = GEAR_META[gearType]; let riskLevel: FishingAnalysis['riskLevel'] = 'LOW'; let riskReason = '정상 범위'; // 휴어기 체크 if (meta.permitCode !== '-' && !isInPermitPeriod(meta.permitCode, now)) { riskLevel = 'CRITICAL'; riskReason = `휴어기 조업 의심 (${meta.permitCode} 허가기간: ${PERMIT_PERIODS[meta.permitCode]?.label})`; } // AIS 신호 오래된 경우 (다크베셀 의심) const aisAge = Date.now() - ship.lastSeen; if (aisAge > 6 * 3600_000) { riskLevel = 'HIGH'; riskReason = `AIS 공백 ${Math.round(aisAge / 3600_000)}시간 — 다크베셀 의심`; } // 자망 + 조업 중 → AIS 차단 가능성 높음 if (gearType === 'gillnet' && isOperating) { if (riskLevel === 'LOW') { riskLevel = 'MEDIUM'; riskReason = '유자망 조업 중 — AIS 차단 주의 업종'; } } return { gearType, gearTypeKo: meta.ko, permitCode: meta.permitCode, gbCode: meta.gbCode, isOperating, operatingStatusKo, confidence, riskLevel, riskReason, }; } /** * 중국 어선 조업 통계 집계 */ export function aggregateFishingStats(ships: Ship[]) { // 중국 어선(fishing 카테고리)만 대상 const chineseFishing = ships.filter(s => s.flag === 'CN' && s.category === 'fishing'); const results = chineseFishing.map(s => ({ ship: s, analysis: analyzeFishing(s) })); const operating = results.filter(r => r.analysis.isOperating); const byGear: Record = { trawl_pair: 0, trawl_single: 0, gillnet: 0, stow_net: 0, purse_seine: 0, carrier: 0, unknown: 0, }; for (const r of operating) { byGear[r.analysis.gearType]++; } const critical = results.filter(r => r.analysis.riskLevel === 'CRITICAL').length; const high = results.filter(r => r.analysis.riskLevel === 'HIGH').length; return { total: chineseFishing.length, operating: operating.length, idle: chineseFishing.length - operating.length, byGear, critical, high, details: results, }; }