release: vessel-analysis API + 불법어선 필터 수정 #109
@ -58,6 +58,7 @@ public class VesselAnalysisResult {
|
||||
|
||||
private Double spoofingScore;
|
||||
|
||||
@Column(name = "bd09_offset_m")
|
||||
private Double bd09OffsetM;
|
||||
|
||||
private Integer speedJumpCount;
|
||||
|
||||
@ -308,9 +308,17 @@ export function useKoreaFilters(
|
||||
return visibleShips.filter(s => {
|
||||
const mtCat = getMarineTrafficCategory(s.typecode, s.category);
|
||||
if (filters.illegalFishing) {
|
||||
const analysis = analysisMap?.get(s.mmsi);
|
||||
if (analysis) {
|
||||
// Python 분석: 영해/접속수역 침범 또는 위험도 HIGH+ 어선
|
||||
const zone = analysis.algorithms.location.zone;
|
||||
const riskLevel = analysis.algorithms.riskScore.level;
|
||||
const isThreat = zone === 'TERRITORIAL_SEA' || zone === 'CONTIGUOUS_ZONE'
|
||||
|| riskLevel === 'CRITICAL' || riskLevel === 'HIGH';
|
||||
if (isThreat) return true;
|
||||
}
|
||||
// 비한국 어선 (기본 필터)
|
||||
if (mtCat === 'fishing' && s.flag !== 'KR') return true;
|
||||
const riskLevel = analysisMap?.get(s.mmsi)?.algorithms.riskScore.level;
|
||||
if (mtCat === 'fishing' && (riskLevel === 'CRITICAL' || riskLevel === 'HIGH')) return true;
|
||||
}
|
||||
if (filters.illegalTransship && transshipSuspects.has(s.mmsi)) return true;
|
||||
if (filters.darkVessel && darkVesselSet.has(s.mmsi)) return true;
|
||||
|
||||
54
frontend/src/services/chnPrmShip.ts
Normal file
54
frontend/src/services/chnPrmShip.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type { ChnPrmShipInfo } from '../types';
|
||||
|
||||
const SIGNAL_BATCH_BASE = '/signal-batch';
|
||||
const CACHE_TTL_MS = 5 * 60_000; // 5분
|
||||
|
||||
let cachedList: ChnPrmShipInfo[] = [];
|
||||
let cacheTime = 0;
|
||||
let fetchPromise: Promise<ChnPrmShipInfo[]> | null = null;
|
||||
|
||||
async function fetchList(): Promise<ChnPrmShipInfo[]> {
|
||||
const now = Date.now();
|
||||
if (cachedList.length > 0 && now - cacheTime < CACHE_TTL_MS) {
|
||||
return cachedList;
|
||||
}
|
||||
|
||||
if (fetchPromise) return fetchPromise;
|
||||
|
||||
fetchPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${SIGNAL_BATCH_BASE}/api/v2/vessels/chnprmship/recent-positions?minutes=60`,
|
||||
{ headers: { accept: 'application/json' } },
|
||||
);
|
||||
if (!res.ok) return cachedList;
|
||||
const json: unknown = await res.json();
|
||||
cachedList = Array.isArray(json) ? (json as ChnPrmShipInfo[]) : [];
|
||||
cacheTime = Date.now();
|
||||
return cachedList;
|
||||
} catch {
|
||||
return cachedList;
|
||||
} finally {
|
||||
fetchPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/** mmsi로 허가어선 정보 조회 — 목록을 캐시하고 lookup */
|
||||
export async function lookupPermittedShip(mmsi: string): Promise<ChnPrmShipInfo | null> {
|
||||
const list = await fetchList();
|
||||
return list.find((s) => s.mmsi === mmsi) ?? null;
|
||||
}
|
||||
|
||||
/** 허가어선 mmsi Set (빠른 조회용) */
|
||||
export async function getPermittedMmsiSet(): Promise<Set<string>> {
|
||||
const list = await fetchList();
|
||||
return new Set(list.map((s) => s.mmsi));
|
||||
}
|
||||
|
||||
/** 캐시 강제 갱신 */
|
||||
export function invalidateCache(): void {
|
||||
cacheTime = 0;
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user