Merge pull request 'release: vessel-analysis API + 불법어선 필터 수정' (#109) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 2m17s

This commit is contained in:
htlee 2026-03-20 13:58:24 +09:00
커밋 6e12883768
3개의 변경된 파일65개의 추가작업 그리고 2개의 파일을 삭제

파일 보기

@ -58,6 +58,7 @@ public class VesselAnalysisResult {
private Double spoofingScore; private Double spoofingScore;
@Column(name = "bd09_offset_m")
private Double bd09OffsetM; private Double bd09OffsetM;
private Integer speedJumpCount; private Integer speedJumpCount;

파일 보기

@ -308,9 +308,17 @@ export function useKoreaFilters(
return visibleShips.filter(s => { return visibleShips.filter(s => {
const mtCat = getMarineTrafficCategory(s.typecode, s.category); const mtCat = getMarineTrafficCategory(s.typecode, s.category);
if (filters.illegalFishing) { 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; 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.illegalTransship && transshipSuspects.has(s.mmsi)) return true;
if (filters.darkVessel && darkVesselSet.has(s.mmsi)) return true; if (filters.darkVessel && darkVesselSet.has(s.mmsi)) return true;

파일 보기

@ -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;
}