feat: 트랙 API 전체 모델 확장 + 개별 선박 on/off → 폴리곤 반영

Prediction API:
- /correlation/{group}/tracks: is_default=TRUE 제거 → 모든 활성 모델 조회
- 응답에 models: {modelName: score} 딕셔너리 추가 (모델별 점수)
- MMSI 기준 중복 제거, 최고 점수 유지

Frontend:
- CorrelationVesselTrack 타입: models 필드 추가, type 필드 추가
- 오퍼레이셔널 폴리곤: enabledVessels 기반 on/off 제어
  (score 임계값 → 개별 체크박스 토글로 전환)
- identity OFF 시 폴리곤 base points에서 멤버 위치 제외

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-03-31 08:40:31 +09:00
부모 c4186a327d
커밋 8eacbb2c91
3개의 변경된 파일33개의 추가작업 그리고 20개의 파일을 삭제

파일 보기

@ -412,7 +412,7 @@ export function useGearReplayLayers(
} }
} }
// 8. Operational polygons (per model — union of member positions + high-score correlation vessels) // 8. Operational polygons (멤버 위치 + enabledVessels ON인 연관 선박으로 폴리곤 생성)
for (const [mn, items] of correlationByModel) { for (const [mn, items] of correlationByModel) {
if (!enabledModels.has(mn)) continue; if (!enabledModels.has(mn)) continue;
const color = MODEL_COLORS[mn] ?? '#94a3b8'; const color = MODEL_COLORS[mn] ?? '#94a3b8';
@ -420,13 +420,15 @@ export function useGearReplayLayers(
const extraPts: [number, number][] = []; const extraPts: [number, number][] = [];
for (const c of items as GearCorrelationItem[]) { for (const c of items as GearCorrelationItem[]) {
if (c.score < 0.7) continue; // enabledVessels로 개별 on/off 제어 (토글 대응)
if (!enabledVessels.has(c.targetMmsi)) continue;
const cp = corrPositions.find(p => p.mmsi === c.targetMmsi); const cp = corrPositions.find(p => p.mmsi === c.targetMmsi);
if (cp) extraPts.push([cp.lon, cp.lat]); if (cp) extraPts.push([cp.lon, cp.lat]);
} }
if (extraPts.length === 0) continue; if (extraPts.length === 0) continue;
const opPolygon = buildInterpPolygon([...memberPts, ...extraPts]); const basePts = enabledModels.has('identity') ? memberPts : [];
const opPolygon = buildInterpPolygon([...basePts, ...extraPts]);
if (!opPolygon) continue; if (!opPolygon) continue;
layers.push(new PolygonLayer({ layers.push(new PolygonLayer({

파일 보기

@ -144,8 +144,9 @@ export interface CorrelationTrackPoint {
export interface CorrelationVesselTrack { export interface CorrelationVesselTrack {
mmsi: string; mmsi: string;
name: string; name: string;
type: string; // 'VESSEL' | 'GEAR'
score: number; score: number;
modelName: string; models: Record<string, number>; // { modelName: score }
track: CorrelationTrackPoint[]; track: CorrelationTrackPoint[];
} }

파일 보기

@ -78,8 +78,9 @@ def get_correlation_tracks(
): ):
"""Return correlated vessels with their track history for map rendering. """Return correlated vessels with their track history for map rendering.
Queries gear_correlation_scores (default model) and enriches with Queries gear_correlation_scores (ALL active models) and enriches with
24h track data from in-memory vessel_store. 24h track data from in-memory vessel_store.
Each vessel includes which models detected it.
""" """
from cache.vessel_store import vessel_store from cache.vessel_store import vessel_store
@ -87,7 +88,7 @@ def get_correlation_tracks(
conn = kcgdb.get_conn() conn = kcgdb.get_conn()
cur = conn.cursor() cur = conn.cursor()
# Get correlated vessels from default model # Get correlated vessels from ALL active models
cur.execute(""" cur.execute("""
SELECT s.target_mmsi, s.target_type, s.target_name, SELECT s.target_mmsi, s.target_type, s.target_name,
s.current_score, m.name AS model_name s.current_score, m.name AS model_name
@ -95,7 +96,6 @@ def get_correlation_tracks(
JOIN kcg.correlation_param_models m ON s.model_id = m.id JOIN kcg.correlation_param_models m ON s.model_id = m.id
WHERE s.group_key = %s WHERE s.group_key = %s
AND s.current_score >= %s AND s.current_score >= %s
AND m.is_default = TRUE
AND m.is_active = TRUE AND m.is_active = TRUE
ORDER BY s.current_score DESC ORDER BY s.current_score DESC
""", (group_key, min_score)) """, (group_key, min_score))
@ -107,31 +107,41 @@ def get_correlation_tracks(
if not rows: if not rows:
return {'groupKey': group_key, 'vessels': []} return {'groupKey': group_key, 'vessels': []}
# Collect target MMSIs # Group by MMSI: collect all models per vessel, keep highest score
vessel_info = [] vessel_map: dict[str, dict] = {}
mmsis = []
for row in rows: for row in rows:
vessel_info.append({ mmsi = row[0]
'mmsi': row[0], model_name = row[4]
'type': row[1], score = float(row[3])
'name': row[2] or '', if mmsi not in vessel_map:
'score': float(row[3]), vessel_map[mmsi] = {
'modelName': row[4], 'mmsi': mmsi,
}) 'type': row[1],
mmsis.append(row[0]) 'name': row[2] or '',
'score': score,
'models': {model_name: score},
}
else:
entry = vessel_map[mmsi]
entry['models'][model_name] = score
if score > entry['score']:
entry['score'] = score
mmsis = list(vessel_map.keys())
# Get tracks from vessel_store # Get tracks from vessel_store
tracks = vessel_store.get_vessel_tracks(mmsis, hours) tracks = vessel_store.get_vessel_tracks(mmsis, hours)
# Build response # Build response
vessels = [] vessels = []
for info in vessel_info: for info in vessel_map.values():
track = tracks.get(info['mmsi'], []) track = tracks.get(info['mmsi'], [])
vessels.append({ vessels.append({
'mmsi': info['mmsi'], 'mmsi': info['mmsi'],
'name': info['name'], 'name': info['name'],
'type': info['type'],
'score': info['score'], 'score': info['score'],
'modelName': info['modelName'], 'models': info['models'], # {modelName: score, ...}
'track': track, 'track': track,
}) })