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();