Merge pull request 'fix: 이름 기반 레이어 항상 ON + 최상위 z-index' (#208) from feature/identity-layer-fix into develop

This commit is contained in:
htlee 2026-03-31 10:09:41 +09:00
커밋 b14df41da7
3개의 변경된 파일60개의 추가작업 그리고 72개의 파일을 삭제

파일 보기

@ -21,6 +21,7 @@
- enabledVessels 토글 시 폴리곤 + 중심경로 동시 재계산
### 수정
- 이름 기반 레이어 항상 ON 고정 + 최상위 z-index (다른 모델에 가려지지 않음)
- Prediction API DB 접속 context manager 누락
- Prediction proxy rewrite 경로 불일치 (/api/prediction → /api)
- nginx prediction API 라우팅 추가

파일 보기

@ -236,18 +236,13 @@ const CorrelationPanel = ({
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 9, cursor: 'pointer', marginBottom: 3 }}>
<input
type="checkbox"
checked={enabledModels.has('identity')}
onChange={() => onEnabledModelsChange(prev => {
const next = new Set(prev);
if (next.has('identity')) next.delete('identity');
else next.add('identity');
return next;
})}
style={{ accentColor: '#f97316', width: 11, height: 11 }}
title="이름 기반"
checked={true}
disabled
style={{ accentColor: '#f97316', width: 11, height: 11, opacity: 0.6 }}
title="이름 기반 (항상 ON)"
/>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f97316', flexShrink: 0 }} />
<span style={{ color: '#e2e8f0' }}> </span>
<span style={{ color: '#94a3b8' }}> ()</span>
<span style={{ color: '#64748b', marginLeft: 'auto' }}>{memberCount}</span>
</label>
{correlationLoading && <div style={{ fontSize: 8, color: '#64748b' }}>...</div>}

파일 보기

@ -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<string>();
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<MemberPosition>({
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,13 +533,12 @@ export function useGearReplayLayers(
const badgeTargets = new Map<string, { lon: number; lat: number; models: Set<string> }>();
// Identity model: group members
if (enabledModels.has('identity')) {
// Identity — 항상 ON
for (const m of members) {
const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set<string>() };
e.lon = m.lon; e.lat = m.lat; e.models.add('identity');
badgeTargets.set(m.mmsi, e);
}
}
// Correlation models
for (const [mn, items] of correlationByModel) {
@ -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();