diff --git a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java index 17f117b..af85801 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java +++ b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java @@ -7,5 +7,8 @@ import java.util.List; public interface VesselAnalysisResultRepository extends JpaRepository { - List findByTimestampAfter(Instant since); + List findByAnalyzedAtAfter(Instant since); + + /** 가장 최근 analyzed_at 이후 결과 전체 (최신 분석 사이클) */ + List findByAnalyzedAtGreaterThanEqual(Instant since); } diff --git a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java index 9e78162..4cac4d1 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java +++ b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java @@ -14,13 +14,11 @@ import java.util.List; @RequiredArgsConstructor public class VesselAnalysisService { - private static final int RECENT_MINUTES = 10; - private final VesselAnalysisResultRepository repository; private final CacheManager cacheManager; /** - * 최근 10분 내 분석 결과를 반환한다. Caffeine 캐시(TTL 5분) 적용. + * 최근 1시간 내 분석 결과를 반환한다. Caffeine 캐시(TTL 5분) 적용. */ @SuppressWarnings("unchecked") public List getLatestResults() { @@ -32,8 +30,9 @@ public class VesselAnalysisService { } } - Instant since = Instant.now().minus(RECENT_MINUTES, ChronoUnit.MINUTES); - List results = repository.findByTimestampAfter(since) + // 최근 1시간 이내 분석 결과 (Python 5분 주기 → 최대 12사이클 포함) + Instant since = Instant.now().minus(1, ChronoUnit.HOURS); + List results = repository.findByAnalyzedAtAfter(since) .stream() .map(VesselAnalysisDto::from) .toList(); diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index f0eb4b4..b0d6ffd 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -24,6 +24,8 @@ import { ChineseFishingOverlay } from './ChineseFishingOverlay'; import { AnalysisOverlay } from './AnalysisOverlay'; import { FishingZoneLayer } from './FishingZoneLayer'; import { AnalysisStatsPanel } from './AnalysisStatsPanel'; +import { getMarineTrafficCategory } from '../../utils/marineTraffic'; +import { classifyFishingZone } from '../../utils/fishingAnalysis'; import { fetchKoreaInfra } from '../../services/infra'; import type { PowerFacility } from '../../services/infra'; import type { Ship, Aircraft, SatellitePosition } from '../../types'; @@ -206,6 +208,30 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre {layers.ships && } + {/* Illegal fishing vessel markers */} + {koreaFilters.illegalFishing && ships.filter(s => { + const mtCat = getMarineTrafficCategory(s.typecode, s.category); + if (mtCat !== 'fishing' || s.flag === 'KR') return false; + return classifyFishingZone(s.lat, s.lng).zone !== 'OUTSIDE'; + }).slice(0, 200).map(s => ( + +
+
+ {s.name || s.mmsi} +
+ + ))} {/* Transship suspect labels — Marker DOM, inline styles kept for dynamic border color */} {transshipSuspects.size > 0 && ships.filter(s => transshipSuspects.has(s.mmsi)).map(s => ( @@ -350,8 +376,8 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre
)} - {/* AI Analysis Stats Panel */} - {vesselAnalysis && vesselAnalysis.stats.total > 0 && ( + {/* AI Analysis Stats Panel — 항상 표시 */} + {vesselAnalysis && ( { const mtCat = getMarineTrafficCategory(s.typecode, s.category); if (filters.illegalFishing) { + // 특정어업수역 Ⅰ~Ⅳ 내 비한국 어선만 불법어선으로 판별 + if (mtCat === 'fishing' && s.flag !== 'KR') { + const zoneInfo = classifyFishingZone(s.lat, s.lng); + if (zoneInfo.zone !== 'OUTSIDE') return true; + } + // Python 분석: 영해/접속수역 침범 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 (zone === 'TERRITORIAL_SEA' || zone === 'CONTIGUOUS_ZONE') return true; } - // 비한국 어선 (기본 필터) - if (mtCat === 'fishing' && s.flag !== 'KR') return true; } if (filters.illegalTransship && transshipSuspects.has(s.mmsi)) return true; if (filters.darkVessel && darkVesselSet.has(s.mmsi)) return true;