kcg-monitoring/frontend/src/hooks/useVesselAnalysis.ts
2026-03-20 13:28:50 +09:00

115 lines
3.3 KiB
TypeScript

import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import type { VesselAnalysisDto, RiskLevel } from '../types';
import { fetchVesselAnalysis } from '../services/vesselAnalysis';
const POLL_INTERVAL_MS = 5 * 60_000; // 5분
const STALE_MS = 30 * 60_000; // 30분
export interface AnalysisStats {
total: number;
critical: number;
high: number;
medium: number;
low: number;
dark: number;
spoofing: number;
clusterCount: number;
}
export interface UseVesselAnalysisResult {
analysisMap: Map<string, VesselAnalysisDto>;
stats: AnalysisStats;
clusters: Map<number, string[]>;
isLoading: boolean;
lastUpdated: number;
}
const EMPTY_STATS: AnalysisStats = {
total: 0, critical: 0, high: 0, medium: 0, low: 0,
dark: 0, spoofing: 0, clusterCount: 0,
};
export function useVesselAnalysis(enabled: boolean): UseVesselAnalysisResult {
const mapRef = useRef<Map<string, VesselAnalysisDto>>(new Map());
const [version, setVersion] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [lastUpdated, setLastUpdated] = useState(0);
const doFetch = useCallback(async () => {
if (!enabled) return;
setIsLoading(true);
try {
const items = await fetchVesselAnalysis();
const now = Date.now();
const map = mapRef.current;
// stale 제거
for (const [mmsi, dto] of map) {
const ts = new Date(dto.timestamp).getTime();
if (now - ts > STALE_MS) map.delete(mmsi);
}
// 새 결과 merge
for (const item of items) {
map.set(item.mmsi, item);
}
setLastUpdated(now);
setVersion(v => v + 1);
} catch {
// 에러 시 기존 데이터 유지 (graceful degradation)
} finally {
setIsLoading(false);
}
}, [enabled]);
useEffect(() => {
doFetch();
const t = setInterval(doFetch, POLL_INTERVAL_MS);
return () => clearInterval(t);
}, [doFetch]);
const analysisMap = mapRef.current;
const stats = useMemo((): AnalysisStats => {
if (analysisMap.size === 0) return EMPTY_STATS;
let critical = 0, high = 0, medium = 0, low = 0, dark = 0, spoofing = 0;
const clusterIds = new Set<number>();
for (const dto of analysisMap.values()) {
const level: RiskLevel = dto.algorithms.riskScore.level;
if (level === 'CRITICAL') critical++;
else if (level === 'HIGH') high++;
else if (level === 'MEDIUM') medium++;
else low++;
if (dto.algorithms.darkVessel.isDark) dark++;
if (dto.algorithms.gpsSpoofing.spoofingScore > 0.5) spoofing++;
if (dto.algorithms.cluster.clusterId >= 0) {
clusterIds.add(dto.algorithms.cluster.clusterId);
}
}
return {
total: analysisMap.size, critical, high, medium, low,
dark, spoofing, clusterCount: clusterIds.size,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [version]);
const clusters = useMemo((): Map<number, string[]> => {
const result = new Map<number, string[]>();
for (const [mmsi, dto] of analysisMap) {
const cid = dto.algorithms.cluster.clusterId;
if (cid < 0) continue;
const arr = result.get(cid);
if (arr) arr.push(mmsi);
else result.set(cid, [mmsi]);
}
return result;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [version]);
return { analysisMap, stats, clusters, isLoading, lastUpdated };
}