From 8eacbb2c916cf9133017b8da2ce99a4236b2e242 Mon Sep 17 00:00:00 2001 From: htlee Date: Tue, 31 Mar 2026 08:40:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8A=B8=EB=9E=99=20API=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=AA=A8=EB=8D=B8=20=ED=99=95=EC=9E=A5=20+=20?= =?UTF-8?q?=EA=B0=9C=EB=B3=84=20=EC=84=A0=EB=B0=95=20on/off=20=E2=86=92=20?= =?UTF-8?q?=ED=8F=B4=EB=A6=AC=EA=B3=A4=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/hooks/useGearReplayLayers.ts | 8 +++-- frontend/src/services/vesselAnalysis.ts | 3 +- prediction/main.py | 42 ++++++++++++++--------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/frontend/src/hooks/useGearReplayLayers.ts b/frontend/src/hooks/useGearReplayLayers.ts index a420228..cfd29b9 100644 --- a/frontend/src/hooks/useGearReplayLayers.ts +++ b/frontend/src/hooks/useGearReplayLayers.ts @@ -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) { if (!enabledModels.has(mn)) continue; const color = MODEL_COLORS[mn] ?? '#94a3b8'; @@ -420,13 +420,15 @@ export function useGearReplayLayers( const extraPts: [number, number][] = []; 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); if (cp) extraPts.push([cp.lon, cp.lat]); } if (extraPts.length === 0) continue; - const opPolygon = buildInterpPolygon([...memberPts, ...extraPts]); + const basePts = enabledModels.has('identity') ? memberPts : []; + const opPolygon = buildInterpPolygon([...basePts, ...extraPts]); if (!opPolygon) continue; layers.push(new PolygonLayer({ diff --git a/frontend/src/services/vesselAnalysis.ts b/frontend/src/services/vesselAnalysis.ts index 5eea1b7..e18b9c1 100644 --- a/frontend/src/services/vesselAnalysis.ts +++ b/frontend/src/services/vesselAnalysis.ts @@ -144,8 +144,9 @@ export interface CorrelationTrackPoint { export interface CorrelationVesselTrack { mmsi: string; name: string; + type: string; // 'VESSEL' | 'GEAR' score: number; - modelName: string; + models: Record; // { modelName: score } track: CorrelationTrackPoint[]; } diff --git a/prediction/main.py b/prediction/main.py index 30ae293..00c9b33 100644 --- a/prediction/main.py +++ b/prediction/main.py @@ -78,8 +78,9 @@ def get_correlation_tracks( ): """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. + Each vessel includes which models detected it. """ from cache.vessel_store import vessel_store @@ -87,7 +88,7 @@ def get_correlation_tracks( conn = kcgdb.get_conn() cur = conn.cursor() - # Get correlated vessels from default model + # Get correlated vessels from ALL active models cur.execute(""" SELECT s.target_mmsi, s.target_type, s.target_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 WHERE s.group_key = %s AND s.current_score >= %s - AND m.is_default = TRUE AND m.is_active = TRUE ORDER BY s.current_score DESC """, (group_key, min_score)) @@ -107,31 +107,41 @@ def get_correlation_tracks( if not rows: return {'groupKey': group_key, 'vessels': []} - # Collect target MMSIs - vessel_info = [] - mmsis = [] + # Group by MMSI: collect all models per vessel, keep highest score + vessel_map: dict[str, dict] = {} for row in rows: - vessel_info.append({ - 'mmsi': row[0], - 'type': row[1], - 'name': row[2] or '', - 'score': float(row[3]), - 'modelName': row[4], - }) - mmsis.append(row[0]) + mmsi = row[0] + model_name = row[4] + score = float(row[3]) + if mmsi not in vessel_map: + vessel_map[mmsi] = { + 'mmsi': mmsi, + 'type': row[1], + '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 tracks = vessel_store.get_vessel_tracks(mmsis, hours) # Build response vessels = [] - for info in vessel_info: + for info in vessel_map.values(): track = tracks.get(info['mmsi'], []) vessels.append({ 'mmsi': info['mmsi'], 'name': info['name'], + 'type': info['type'], 'score': info['score'], - 'modelName': info['modelName'], + 'models': info['models'], # {modelName: score, ...} 'track': track, })