Merge pull request 'release: 불법어선 수역 필터 + AI 패널 + 마커' (#113) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 1m49s
All checks were successful
Deploy KCG / deploy (push) Successful in 1m49s
This commit is contained in:
커밋
b24d43e4a1
@ -7,5 +7,8 @@ import java.util.List;
|
||||
|
||||
public interface VesselAnalysisResultRepository extends JpaRepository<VesselAnalysisResult, Long> {
|
||||
|
||||
List<VesselAnalysisResult> findByTimestampAfter(Instant since);
|
||||
List<VesselAnalysisResult> findByAnalyzedAtAfter(Instant since);
|
||||
|
||||
/** 가장 최근 analyzed_at 이후 결과 전체 (최신 분석 사이클) */
|
||||
List<VesselAnalysisResult> findByAnalyzedAtGreaterThanEqual(Instant since);
|
||||
}
|
||||
|
||||
@ -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<VesselAnalysisDto> getLatestResults() {
|
||||
@ -32,8 +30,9 @@ public class VesselAnalysisService {
|
||||
}
|
||||
}
|
||||
|
||||
Instant since = Instant.now().minus(RECENT_MINUTES, ChronoUnit.MINUTES);
|
||||
List<VesselAnalysisDto> results = repository.findByTimestampAfter(since)
|
||||
// 최근 1시간 이내 분석 결과 (Python 5분 주기 → 최대 12사이클 포함)
|
||||
Instant since = Instant.now().minus(1, ChronoUnit.HOURS);
|
||||
List<VesselAnalysisDto> results = repository.findByAnalyzedAtAfter(since)
|
||||
.stream()
|
||||
.map(VesselAnalysisDto::from)
|
||||
.toList();
|
||||
|
||||
@ -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
|
||||
</Source>
|
||||
|
||||
{layers.ships && <ShipLayer ships={ships} militaryOnly={layers.militaryOnly} />}
|
||||
{/* 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 => (
|
||||
<Marker key={`illegal-${s.mmsi}`} longitude={s.lng} latitude={s.lat} anchor="center">
|
||||
<div style={{
|
||||
width: 20, height: 20, borderRadius: '50%',
|
||||
border: '2px solid #ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.2)',
|
||||
animation: 'pulse 2s infinite',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
<div style={{
|
||||
fontSize: 8, fontWeight: 700, color: '#ef4444',
|
||||
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||
textAlign: 'center', marginTop: -2,
|
||||
whiteSpace: 'nowrap', pointerEvents: 'none',
|
||||
}}>
|
||||
{s.name || s.mmsi}
|
||||
</div>
|
||||
</Marker>
|
||||
))}
|
||||
{/* Transship suspect labels — Marker DOM, inline styles kept for dynamic border color */}
|
||||
{transshipSuspects.size > 0 && ships.filter(s => transshipSuspects.has(s.mmsi)).map(s => (
|
||||
<Marker key={`ts-${s.mmsi}`} longitude={s.lng} latitude={s.lat} anchor="bottom">
|
||||
@ -350,8 +376,8 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Analysis Stats Panel */}
|
||||
{vesselAnalysis && vesselAnalysis.stats.total > 0 && (
|
||||
{/* AI Analysis Stats Panel — 항상 표시 */}
|
||||
{vesselAnalysis && (
|
||||
<AnalysisStatsPanel
|
||||
stats={vesselAnalysis.stats}
|
||||
lastUpdated={vesselAnalysis.lastUpdated}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import { KOREA_SUBMARINE_CABLES } from '../services/submarineCable';
|
||||
import { getMarineTrafficCategory } from '../utils/marineTraffic';
|
||||
import { classifyFishingZone } from '../utils/fishingAnalysis';
|
||||
import type { Ship, VesselAnalysisDto } from '../types';
|
||||
|
||||
interface KoreaFilters {
|
||||
@ -308,17 +309,17 @@ export function useKoreaFilters(
|
||||
return visibleShips.filter(s => {
|
||||
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;
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user