- 어선 분류 개선: 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>
231 lines
7.3 KiB
TypeScript
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,
|
|
};
|
|
}
|