diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 388cf2b..2266cad 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -21,6 +21,7 @@ - enabledVessels 토글 시 폴리곤 + 중심경로 동시 재계산 ### 수정 +- 이름 기반 레이어 항상 ON 고정 + 최상위 z-index (다른 모델에 가려지지 않음) - Prediction API DB 접속 context manager 누락 - Prediction proxy rewrite 경로 불일치 (/api/prediction → /api) - nginx prediction API 라우팅 추가 diff --git a/frontend/src/components/korea/CorrelationPanel.tsx b/frontend/src/components/korea/CorrelationPanel.tsx index 709bf0a..66ad9e4 100644 --- a/frontend/src/components/korea/CorrelationPanel.tsx +++ b/frontend/src/components/korea/CorrelationPanel.tsx @@ -236,18 +236,13 @@ const CorrelationPanel = ({ {correlationLoading &&
로딩...
} diff --git a/frontend/src/hooks/useGearReplayLayers.ts b/frontend/src/hooks/useGearReplayLayers.ts index 1ad21bf..d121090 100644 --- a/frontend/src/hooks/useGearReplayLayers.ts +++ b/frontend/src/hooks/useGearReplayLayers.ts @@ -144,8 +144,8 @@ export function useGearReplayLayers( // ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ───────────── if (showTrails) { - // 멤버 전체 항적 (identity) - if (enabledModels.has('identity') && memberTripsData.length > 0) { + // 멤버 전체 항적 (identity — 항상 ON) + if (memberTripsData.length > 0) { for (const trip of memberTripsData) { if (trip.path.length < 2) continue; layers.push(new PathLayer({ @@ -179,41 +179,7 @@ export function useGearReplayLayers( } } - // 1. Identity 모델: TripsLayer + 폴리곤 + 마커 (항상 ON) - if (enabledModels.has('identity')) { - // TripsLayer — member trails (GPU animated, 항상 ON, 고채도) - if (memberTripsData.length > 0) { - layers.push(new TripsLayer({ - id: 'replay-member-trails', - data: memberTripsData, - getPath: d => d.path, - getTimestamps: d => d.timestamps, - getColor: [255, 200, 60, 220], // 고채도 노란색 (항적보다 밝게) - widthMinPixels: 2, - fadeTrail: true, - trailLength: TRAIL_LENGTH_MS, - currentTime: ct - st, - })); - } - - // Current animated polygon (convex hull of members) - const polygon = buildInterpPolygon(memberPts); - if (polygon) { - layers.push(new PolygonLayer({ - id: 'replay-polygon', - data: [{ polygon: polygon.coordinates }], - getPolygon: (d: { polygon: number[][][] }) => d.polygon, - getFillColor: isStale ? [148, 163, 184, 30] : [251, 191, 36, 40], - getLineColor: isStale ? [148, 163, 184, 100] : [251, 191, 36, 180], - getLineWidth: isStale ? 1 : 2, - lineWidthMinPixels: 1, - filled: true, - stroked: true, - })); - } - } - - // 2. Correlation TripsLayer (GPU animated, 항상 ON, 고채도) + // 1. Correlation TripsLayer (GPU animated, 항상 ON, 고채도) if (correlationTripsData.length > 0) { const activeMmsis = new Set(); for (const [mn, items] of correlationByModel) { @@ -238,22 +204,10 @@ export function useGearReplayLayers( } } - // 4. Current center point - layers.push(new ScatterplotLayer({ - id: 'replay-center', - data: [{ position: [frame.centerLon, frame.centerLat] as [number, number] }], - getPosition: (d: { position: [number, number] }) => d.position, - getFillColor: isStale ? [249, 115, 22, 255] : [239, 68, 68, 255], - getRadius: 200, - radiusUnits: 'meters', - radiusMinPixels: 7, - stroked: true, - getLineColor: [255, 255, 255, 255], - lineWidthMinPixels: 2, - })); + // (identity 레이어는 최하단 — 최상위 z-index로 이동됨) - // 5. Member position markers (IconLayer, identity 모델 활성 시만) - if (members.length > 0 && enabledModels.has('identity')) { + // 3. Member position markers (IconLayer, identity — 항상 ON, placeholder) + if (members.length > 0) { layers.push(new IconLayer({ id: 'replay-members', data: members, @@ -501,7 +455,7 @@ export function useGearReplayLayers( } if (extraPts.length === 0) continue; - const basePts = enabledModels.has('identity') ? memberPts : []; + const basePts = memberPts; // identity 항상 ON const opPolygon = buildInterpPolygon([...basePts, ...extraPts]); if (!opPolygon) continue; @@ -579,12 +533,11 @@ export function useGearReplayLayers( const badgeTargets = new Map }>(); // Identity model: group members - if (enabledModels.has('identity')) { - for (const m of members) { - const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set() }; - e.lon = m.lon; e.lat = m.lat; e.models.add('identity'); - badgeTargets.set(m.mmsi, e); - } + // Identity — 항상 ON + for (const m of members) { + const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set() }; + e.lon = m.lon; e.lat = m.lat; e.models.add('identity'); + badgeTargets.set(m.mmsi, e); } // Correlation models @@ -627,10 +580,49 @@ export function useGearReplayLayers( } } - // 디버그: 연관 선박 렌더링 상태 - if (corrPositions.length > 0 && !debugLoggedRef.current) { - console.log('[GearReplay] corrPositions:', corrPositions.length, 'operationalPolygons:', layers.filter(l => l.id?.toString().startsWith('replay-op-')).length); + // ══ Identity 레이어 (최상위 z-index — 항상 ON, 다른 모델 위에 표시) ══ + // 폴리곤 + const identityPolygon = buildInterpPolygon(memberPts); + if (identityPolygon) { + layers.push(new PolygonLayer({ + id: 'replay-identity-polygon', + data: [{ polygon: identityPolygon.coordinates }], + getPolygon: (d: { polygon: number[][][] }) => d.polygon, + getFillColor: isStale ? [148, 163, 184, 30] : [251, 191, 36, 40], + getLineColor: isStale ? [148, 163, 184, 100] : [251, 191, 36, 180], + getLineWidth: isStale ? 1 : 2, + lineWidthMinPixels: 1, + filled: true, + stroked: true, + })); } + // TripsLayer (멤버 트레일) + if (memberTripsData.length > 0) { + layers.push(new TripsLayer({ + id: 'replay-identity-trails', + data: memberTripsData, + getPath: d => d.path, + getTimestamps: d => d.timestamps, + getColor: [255, 200, 60, 220], + widthMinPixels: 2, + fadeTrail: true, + trailLength: TRAIL_LENGTH_MS, + currentTime: ct - st, + })); + } + // 센터 포인트 + layers.push(new ScatterplotLayer({ + id: 'replay-identity-center', + data: [{ position: [frame.centerLon, frame.centerLat] as [number, number] }], + getPosition: (d: { position: [number, number] }) => d.position, + getFillColor: isStale ? [249, 115, 22, 255] : [239, 68, 68, 255], + getRadius: 200, + radiusUnits: 'meters', + radiusMinPixels: 7, + stroked: true, + getLineColor: [255, 255, 255, 255], + lineWidthMinPixels: 2, + })); replayLayerRef.current = layers; requestRender();