fix: 순차 데이터 로딩 + enabledModels 토글 제어
데이터 로딩: - loadHistory: Promise.all로 history/correlation/tracks 병렬 fetch → 모든 응답 완료 후 store.loadHistory + play() 순차 실행 - 개별 fetch effect는 비재생 모드에서만 실행 (historyActive 가드) - 타이밍 문제 원천 제거 (race condition 없음) enabledModels 토글 제어: - identity OFF → 멤버 폴리곤/마커/트레일 숨김 - 각 모델 OFF → 해당 모델의 연관 선박/트레일 숨김 - 모든 모델 OFF → 센터 트레일/점만 표시 - correlation trails도 활성 모델에 속하는 선박만 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
e68f314093
커밋
6ba3db5cee
@ -69,17 +69,31 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS
|
||||
fetchFleetCompanies().then(setCompanies).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// ── 히스토리 로드/닫기 ──
|
||||
// ── 히스토리 로드 (3개 API 순차 await → 스토어 초기화 → 재생) ──
|
||||
const loadHistory = async (groupKey: string) => {
|
||||
const history = await fetchGroupHistory(groupKey, 12);
|
||||
// 1. 모든 데이터를 병렬 fetch
|
||||
const [history, corrRes, trackRes] = await Promise.all([
|
||||
fetchGroupHistory(groupKey, 12),
|
||||
fetchGroupCorrelations(groupKey, 0.3).catch(() => ({ items: [] as GearCorrelationItem[] })),
|
||||
fetchCorrelationTracks(groupKey, 24, 0.3).catch(() => ({ vessels: [] as CorrelationVesselTrack[] })),
|
||||
]);
|
||||
|
||||
// 2. 데이터 전처리
|
||||
const sorted = history.reverse();
|
||||
const filled = fillGapFrames(sorted);
|
||||
const corrData = corrRes.items;
|
||||
const corrTracks = trackRes.vessels;
|
||||
const vessels = new Set(corrTracks.filter(v => v.score >= 0.7).map(v => v.mmsi));
|
||||
|
||||
// 3. React 상태 동기화 (패널 표시용)
|
||||
setCorrelationData(corrData);
|
||||
setCorrelationTracks(corrTracks);
|
||||
setEnabledVessels(vessels);
|
||||
setCorrelationLoading(false);
|
||||
|
||||
// 4. 스토어 초기화 (모든 데이터 포함) → 재생 시작
|
||||
const store = useGearReplayStore.getState();
|
||||
store.loadHistory(filled, correlationTracks, correlationData, enabledModels, enabledVessels);
|
||||
// correlation 데이터가 이미 로드된 경우 즉시 동기화
|
||||
if (correlationData.length > 0 || correlationTracks.length > 0) {
|
||||
store.updateCorrelation(correlationData, correlationTracks);
|
||||
}
|
||||
store.loadHistory(filled, corrTracks, corrData, enabledModels, vessels);
|
||||
store.play();
|
||||
};
|
||||
|
||||
@ -101,14 +115,6 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS
|
||||
useGearReplayStore.getState().setHoveredMmsi(hoveredTarget?.mmsi ?? null);
|
||||
}, [hoveredTarget]);
|
||||
|
||||
// ── correlation 데이터 → store 동기화 ──
|
||||
// historyActive 의존: history 로드 후 이미 도착한 correlation 데이터 반영
|
||||
useEffect(() => {
|
||||
if (historyActive && (correlationData.length > 0 || correlationTracks.length > 0)) {
|
||||
useGearReplayStore.getState().updateCorrelation(correlationData, correlationTracks);
|
||||
}
|
||||
}, [correlationData, correlationTracks, historyActive]);
|
||||
|
||||
// ── ESC 키 ──
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
@ -288,9 +294,9 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS
|
||||
onSelectedGearChange?.({ parent: parent ? toShip(parent) : null, gears: gears.map(toShip), groupName: selectedGearGroup });
|
||||
}, [selectedGearGroup, groupPolygons, onSelectedGearChange, historyActive]);
|
||||
|
||||
// ── 연관성 데이터 로드 ──
|
||||
// ── 연관성 데이터 로드 (loadHistory에서 일괄 처리, 여기서는 비재생 모드만) ──
|
||||
useEffect(() => {
|
||||
if (!selectedGearGroup) { setCorrelationData([]); return; }
|
||||
if (!selectedGearGroup || historyActive) { if (!selectedGearGroup) { setCorrelationData([]); } return; }
|
||||
let cancelled = false;
|
||||
setCorrelationLoading(true);
|
||||
fetchGroupCorrelations(selectedGearGroup, 0.3)
|
||||
@ -298,11 +304,11 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS
|
||||
.catch(() => { if (!cancelled) setCorrelationData([]); })
|
||||
.finally(() => { if (!cancelled) setCorrelationLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [selectedGearGroup]);
|
||||
}, [selectedGearGroup, historyActive]);
|
||||
|
||||
// ── 연관 선박 항적 로드 ──
|
||||
// ── 연관 선박 항적 로드 (loadHistory에서 일괄 처리, 여기서는 비재생 모드만) ──
|
||||
useEffect(() => {
|
||||
if (!selectedGearGroup) { setCorrelationTracks([]); setEnabledVessels(new Set()); return; }
|
||||
if (!selectedGearGroup || historyActive) { if (!selectedGearGroup) { setCorrelationTracks([]); setEnabledVessels(new Set()); } return; }
|
||||
let cancelled = false;
|
||||
fetchCorrelationTracks(selectedGearGroup, 24, 0.3)
|
||||
.then(res => {
|
||||
@ -313,7 +319,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS
|
||||
})
|
||||
.catch(() => { if (!cancelled) setCorrelationTracks([]); });
|
||||
return () => { cancelled = true; };
|
||||
}, [selectedGearGroup]);
|
||||
}, [selectedGearGroup, historyActive]);
|
||||
|
||||
// ── 부모 콜백 동기화: 선단 선택 ──
|
||||
useEffect(() => {
|
||||
|
||||
@ -147,27 +147,55 @@ export function useGearReplayLayers(
|
||||
const frame = state.historyFrames[frameIdx];
|
||||
const isStale = !!frame._longGap || !!frame._interp;
|
||||
|
||||
// Member positions (interpolated)
|
||||
// Member positions (interpolated) — 항상 계산 (배지/오퍼레이셔널에서 사용)
|
||||
const members = interpolateMemberPositions(state.historyFrames, frameIdx, ct);
|
||||
const memberPts: [number, number][] = members.map(m => [m.lon, m.lat]);
|
||||
|
||||
// 1. TripsLayer — member trails (GPU animated)
|
||||
if (memberTripsData.length > 0) {
|
||||
layers.push(new TripsLayer({
|
||||
id: 'replay-member-trails',
|
||||
data: memberTripsData,
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
getColor: d => d.color,
|
||||
widthMinPixels: 1.5,
|
||||
fadeTrail: true,
|
||||
trailLength: TRAIL_LENGTH_MS,
|
||||
currentTime: ct - st,
|
||||
}));
|
||||
// 1. Identity 모델: 멤버 트레일 + 폴리곤 + 마커 (enabledModels 체크)
|
||||
if (enabledModels.has('identity')) {
|
||||
// TripsLayer — member trails (GPU animated)
|
||||
if (memberTripsData.length > 0) {
|
||||
layers.push(new TripsLayer({
|
||||
id: 'replay-member-trails',
|
||||
data: memberTripsData,
|
||||
getPath: d => d.path,
|
||||
getTimestamps: d => d.timestamps,
|
||||
getColor: d => d.color,
|
||||
widthMinPixels: 1.5,
|
||||
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. TripsLayer — correlation trails (GPU animated)
|
||||
// 2. Correlation trails (GPU animated, enabledModels 체크)
|
||||
if (correlationTripsData.length > 0) {
|
||||
const enabledTrips = correlationTripsData.filter(d => enabledVessels.has(d.id));
|
||||
// 활성 모델에 속하는 선박의 트랙만 표시
|
||||
const activeMmsis = new Set<string>();
|
||||
for (const [mn, items] of correlationByModel) {
|
||||
if (!enabledModels.has(mn)) continue;
|
||||
for (const c of items as GearCorrelationItem[]) {
|
||||
if (enabledVessels.has(c.targetMmsi)) activeMmsis.add(c.targetMmsi);
|
||||
}
|
||||
}
|
||||
const enabledTrips = correlationTripsData.filter(d => activeMmsis.has(d.id));
|
||||
if (enabledTrips.length > 0) {
|
||||
layers.push(new TripsLayer({
|
||||
id: 'replay-corr-trails',
|
||||
@ -183,23 +211,6 @@ export function useGearReplayLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Current animated polygon (convex hull of members)
|
||||
const memberPts: [number, number][] = members.map(m => [m.lon, m.lat]);
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
// 4. Current center point
|
||||
layers.push(new ScatterplotLayer({
|
||||
id: 'replay-center',
|
||||
@ -214,8 +225,8 @@ export function useGearReplayLayers(
|
||||
lineWidthMinPixels: 2,
|
||||
}));
|
||||
|
||||
// 5. Member position markers (IconLayer — ship-triangle / gear-diamond)
|
||||
if (members.length > 0) {
|
||||
// 5. Member position markers (IconLayer, identity 모델 활성 시만)
|
||||
if (members.length > 0 && enabledModels.has('identity')) {
|
||||
layers.push(new IconLayer<MemberPosition>({
|
||||
id: 'replay-members',
|
||||
data: members,
|
||||
@ -482,13 +493,17 @@ export function useGearReplayLayers(
|
||||
replayLayerRef, requestRender,
|
||||
]);
|
||||
|
||||
// correlationByModel이 갱신되면 디버그 로그 리셋 (새 데이터 도착 확인)
|
||||
// 데이터/필터 변경 시 디버그 로그 리셋
|
||||
useEffect(() => {
|
||||
debugLoggedRef.current = false;
|
||||
if (correlationByModel.size > 0) {
|
||||
debugLoggedRef.current = false;
|
||||
console.log('[GearReplay] correlationByModel 갱신:', correlationByModel.size, '모델', [...correlationByModel.keys()]);
|
||||
console.log('[GearReplay] 데이터 갱신:', {
|
||||
models: [...correlationByModel.keys()],
|
||||
enabledModels: [...enabledModels],
|
||||
corrTrips: correlationTripsData.length,
|
||||
});
|
||||
}
|
||||
}, [correlationByModel]);
|
||||
}, [correlationByModel, enabledModels, correlationTripsData]);
|
||||
|
||||
// ── zustand.subscribe effect (currentTime → renderFrame) ─────────────────
|
||||
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user