kcg-monitoring/frontend/src/hooks/useKoreaData.ts
Nan Kyung Lee 7174dfd629 feat: 중국어선 조업분석, 어구/어망 분류, 이란 시설, 레이어 재구성
- 어선 분류 개선: AIS Ship Type 30 + category fallback + 선박명 패턴
- 어구/어망 카테고리 신설: 선박명_숫자_ / 선박명% 패턴으로 분류
- 중국어선 조업분석: GC-KCG-2026-001 + CSSA 보고서 기반 (안강망 추가)
- 중국어선 선단 탐지: 본선-부속선 쌍, 운반선 환적, 선망 선단
- 어구/어망 → 모선 연결선 시각화
- 어구 SVG 아이콘 5종 (트롤/자망/안강망/선망/기본)
- 이란 주변국 시설 레이어 (MEFacilityLayer 35개소)
- 사우스파르스 가스전 피격 + 카타르 라스라판 보복 공격 반영
- 한국 해군부대 10개소 추가
- 레이어 재구성: 선박(최상위) → 항공망(항공기+위성) → 해양안전 → 국가기관망
- 어선 국적별 하위 분류 (선박 분류 내 어선 펼치기)
- 오른쪽 패널 접기/펼치기 (한국현황, 중국현황, 조업분석, OSINT)
- 항공망 기본 접힘 처리
- 센서차트 기본 숨김

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:46:27 +09:00

231 lines
7.3 KiB
TypeScript

import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { fetchAircraftFromBackend } from '../services/aircraftApi';
import { fetchSatelliteTLEKorea, propagateAll } from '../services/celestrak';
import { fetchShipsKorea } from '../services/ships';
import { fetchOsintFeed } from '../services/osint';
import type { OsintItem } from '../services/osint';
import { propagateAircraft, propagateShips } from '../services/propagation';
import { getMarineTrafficCategory } from '../utils/marineTraffic';
import type { Aircraft, Ship, Satellite, SatellitePosition } from '../types';
interface UseKoreaDataArgs {
currentTime: number;
isLive: boolean;
hiddenAcCategories: Set<string>;
hiddenShipCategories: Set<string>;
hiddenNationalities: Set<string>;
refreshKey: number;
}
interface UseKoreaDataResult {
aircraft: Aircraft[];
ships: Ship[];
visibleAircraft: Aircraft[];
visibleShips: Ship[];
satPositions: SatellitePosition[];
osintFeed: OsintItem[];
koreaKoreanShips: Ship[];
koreaChineseShips: Ship[];
shipsByCategory: Record<string, number>;
shipsByNationality: Record<string, number>;
fishingByNationality: Record<string, number>;
aircraftByCategory: Record<string, number>;
militaryCount: number;
}
const SHIP_POLL_INTERVAL = 300_000; // 5 min
const SHIP_STALE_MS = 3_600_000; // 60 min
/** Map flag code to nationality group key */
export function getNationalityGroup(flag?: string): string {
if (!flag) return 'unclassified';
if (flag === 'KR') return 'KR';
if (flag === 'CN') return 'CN';
if (flag === 'KP') return 'KP';
if (flag === 'JP') return 'JP';
return 'unclassified';
}
export function useKoreaData({
currentTime,
isLive,
hiddenAcCategories,
hiddenShipCategories,
hiddenNationalities,
refreshKey,
}: UseKoreaDataArgs): UseKoreaDataResult {
const [baseAircraftKorea, setBaseAircraftKorea] = useState<Aircraft[]>([]);
const [baseShipsKorea, setBaseShipsKorea] = useState<Ship[]>([]);
const [satellitesKorea, setSatellitesKorea] = useState<Satellite[]>([]);
const [satPositionsKorea, setSatPositionsKorea] = useState<SatellitePosition[]>([]);
const [osintFeed, setOsintFeed] = useState<OsintItem[]>([]);
const satTimeKoreaRef = useRef(0);
const shipMapRef = useRef<Map<string, Ship>>(new Map());
// Fetch Korea satellite TLE data
useEffect(() => {
fetchSatelliteTLEKorea().then(setSatellitesKorea).catch(() => {});
}, [refreshKey]);
// Fetch Korea aircraft data
useEffect(() => {
const load = async () => {
const result = await fetchAircraftFromBackend('korea');
if (result.length > 0) setBaseAircraftKorea(result);
};
load();
const interval = setInterval(load, 60_000);
return () => clearInterval(interval);
}, [refreshKey]);
// Ship merge with stale cleanup
const mergeShips = useCallback((newShips: Ship[]) => {
const map = shipMapRef.current;
for (const s of newShips) {
map.set(s.mmsi, s);
}
const cutoff = Date.now() - SHIP_STALE_MS;
for (const [mmsi, ship] of map) {
if (ship.lastSeen < cutoff) map.delete(mmsi);
}
setBaseShipsKorea(Array.from(map.values()));
}, []);
// Fetch Korea region ship data: initial 60min, then 5min polling with 6min window
useEffect(() => {
let initialDone = false;
const loadInitial = async () => {
try {
const data = await fetchShipsKorea(60); // 초기: 60분 데이터
if (data.length > 0) {
shipMapRef.current = new Map(data.map(s => [s.mmsi, s]));
setBaseShipsKorea(data);
initialDone = true;
}
} catch { /* keep previous */ }
};
const loadIncremental = async () => {
if (!initialDone) return;
try {
const data = await fetchShipsKorea(6); // polling: 6분 데이터
if (data.length > 0) mergeShips(data);
} catch { /* keep previous */ }
};
loadInitial();
const interval = setInterval(loadIncremental, SHIP_POLL_INTERVAL);
return () => clearInterval(interval);
}, [refreshKey, mergeShips]);
// Fetch OSINT feed for Korea tab
useEffect(() => {
const load = async () => {
try {
const data = await fetchOsintFeed('korea');
if (data.length > 0) setOsintFeed(data);
} catch { /* keep previous */ }
};
load();
const interval = setInterval(load, 120_000);
return () => clearInterval(interval);
}, [refreshKey]);
// Propagate Korea satellite positions
useEffect(() => {
if (satellitesKorea.length === 0) return;
const now = Date.now();
if (now - satTimeKoreaRef.current < 2000) return;
satTimeKoreaRef.current = now;
const positions = propagateAll(satellitesKorea, new Date(currentTime));
setSatPositionsKorea(positions);
}, [satellitesKorea, currentTime]);
// Propagate Korea aircraft (live only — no waypoint propagation needed)
const aircraft = useMemo(() => propagateAircraft(baseAircraftKorea, currentTime), [baseAircraftKorea, currentTime]);
// Korea region ships
const ships = useMemo(
() => propagateShips(baseShipsKorea, currentTime, isLive),
[baseShipsKorea, currentTime, isLive],
);
// Category-filtered data for map rendering
const visibleAircraft = useMemo(
() => aircraft.filter(a => !hiddenAcCategories.has(a.category)),
[aircraft, hiddenAcCategories],
);
const visibleShips = useMemo(
() => ships.filter(s =>
!hiddenShipCategories.has(getMarineTrafficCategory(s.typecode, s.category))
&& !hiddenNationalities.has(getNationalityGroup(s.flag)),
),
[ships, hiddenShipCategories, hiddenNationalities],
);
// Korea region stats
const koreaKoreanShips = useMemo(() => ships.filter(s => s.flag === 'KR'), [ships]);
const koreaChineseShips = useMemo(() => ships.filter(s => s.flag === 'CN'), [ships]);
const shipsByCategory = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of ships) {
const mtCat = getMarineTrafficCategory(s.typecode, s.category);
counts[mtCat] = (counts[mtCat] || 0) + 1;
}
return counts;
}, [ships]);
const shipsByNationality = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of ships) {
const nat = getNationalityGroup(s.flag);
counts[nat] = (counts[nat] || 0) + 1;
}
return counts;
}, [ships]);
const fishingByNationality = useMemo(() => {
const counts: Record<string, number> = {};
for (const s of ships) {
if (getMarineTrafficCategory(s.typecode, s.category) !== 'fishing') continue;
const flag = s.flag || 'unknown';
const group = flag === 'CN' ? 'CN' : flag === 'KR' ? 'KR' : flag === 'JP' ? 'JP' : 'other';
counts[group] = (counts[group] || 0) + 1;
}
return counts;
}, [ships]);
// Korea aircraft stats
const aircraftByCategory = useMemo(() => {
const counts: Record<string, number> = {};
for (const ac of aircraft) {
counts[ac.category] = (counts[ac.category] || 0) + 1;
}
return counts;
}, [aircraft]);
const militaryCount = useMemo(
() => aircraft.filter(a => a.category !== 'civilian' && a.category !== 'unknown').length,
[aircraft],
);
return {
aircraft,
ships,
visibleAircraft,
visibleShips,
satPositions: satPositionsKorea,
osintFeed,
koreaKoreanShips,
koreaChineseShips,
shipsByCategory,
shipsByNationality,
fishingByNationality,
aircraftByCategory,
militaryCount,
};
}