diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx index a5e4a1f..c746e4f 100644 --- a/apps/web/src/pages/dashboard/DashboardPage.tsx +++ b/apps/web/src/pages/dashboard/DashboardPage.tsx @@ -27,6 +27,8 @@ import { Topbar } from "../../widgets/topbar/Topbar"; import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel"; import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel"; import { VesselList } from "../../widgets/vesselList/VesselList"; +import type { ActiveTrack } from "../../entities/vesselTrack/model/types"; +import { fetchVesselTrack } from "../../entities/vesselTrack/api/fetchTrack"; import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel"; import { useWeatherPolling } from "../../features/weatherOverlay/useWeatherPolling"; import { useWeatherOverlay } from "../../features/weatherOverlay/useWeatherOverlay"; @@ -36,9 +38,6 @@ import { DepthLegend } from "../../widgets/legend/DepthLegend"; import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types"; import type { MapStyleSettings } from "../../features/mapSettings/types"; import { fmtDateTimeFull, fmtIsoFull } from "../../shared/lib/datetime"; -import { queryTrackByMmsi } from "../../features/trackReplay/services/trackQueryService"; -import { useTrackQueryStore } from "../../features/trackReplay/stores/trackQueryStore"; -import { GlobalTrackReplayPanel } from "../../widgets/trackReplay/GlobalTrackReplayPanel"; import { buildLegacyHitMap, computeCountsByType, @@ -143,33 +142,27 @@ export function DashboardPage() { const [selectedCableId, setSelectedCableId] = useState(null); // 항적 (vessel track) + const [activeTrack, setActiveTrack] = useState(null); const [trackContextMenu, setTrackContextMenu] = useState<{ x: number; y: number; mmsi: number; vesselName: string } | null>(null); const handleOpenTrackMenu = useCallback((info: { x: number; y: number; mmsi: number; vesselName: string }) => { setTrackContextMenu(info); }, []); const handleCloseTrackMenu = useCallback(() => setTrackContextMenu(null), []); const handleRequestTrack = useCallback(async (mmsi: number, minutes: number) => { - const trackStore = useTrackQueryStore.getState(); - const queryKey = `${mmsi}:${minutes}:${Date.now()}`; - trackStore.beginQuery(queryKey); - try { - const target = targets.find((item) => item.mmsi === mmsi); - const tracks = await queryTrackByMmsi({ - mmsi, - minutes, - shipNameHint: target?.name, - }); - - if (tracks.length > 0) { - trackStore.applyTracksSuccess(tracks, queryKey); + const res = await fetchVesselTrack(mmsi, minutes); + if (res.success && res.data.length > 0) { + const sorted = [...res.data].sort( + (a, b) => new Date(a.messageTimestamp).getTime() - new Date(b.messageTimestamp).getTime(), + ); + setActiveTrack({ mmsi, minutes, points: sorted, fetchedAt: Date.now() }); } else { - trackStore.applyQueryError('항적 데이터가 없습니다.', queryKey); + console.warn('Track: no data', res.message); } } catch (e) { - trackStore.applyQueryError(e instanceof Error ? e.message : '항적 조회에 실패했습니다.', queryKey); + console.warn('Track fetch failed:', e); } - }, [targets]); + }, []); const [settings, setSettings] = usePersistedState(uid, 'map3dSettings', { showShips: true, showDensity: false, showSeamark: false, @@ -772,13 +765,12 @@ export function DashboardPage() { onMapReady={handleMapReady} initialView={mapView} onViewStateChange={setMapView} - activeTrack={null} + activeTrack={activeTrack} trackContextMenu={trackContextMenu} onRequestTrack={handleRequestTrack} onCloseTrackMenu={handleCloseTrackMenu} onOpenTrackMenu={handleOpenTrackMenu} /> - (null); const mapRef = useRef(null); @@ -198,38 +201,20 @@ export function Map3D({ ); // ── Ship data memos ────────────────────────────────────────────────── - const rawShipData = useMemo(() => { + const shipData = useMemo(() => { return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi)); }, [targets]); - const hideLiveShips = useTrackQueryStore((state) => state.hideLiveShips); - - const liveShipFeatures = useLiveShipAdapter(rawShipData, legacyHits ?? null); - const { renderedTargets: batchRenderedTargets } = useLiveShipBatchRender( - mapRef, - liveShipFeatures, - rawShipData, - mapSyncEpoch, - ); - - const shipData = useMemo( - () => (hideLiveShips ? [] : rawShipData), - [hideLiveShips, rawShipData], - ); - const shipByMmsi = useMemo(() => { const byMmsi = new Map(); - for (const t of rawShipData) byMmsi.set(t.mmsi, t); + for (const t of shipData) byMmsi.set(t.mmsi, t); return byMmsi; - }, [rawShipData]); + }, [shipData]); const shipLayerData = useMemo(() => { - if (hideLiveShips) return []; - // Fallback to raw targets when batch result is temporarily empty - // (e.g. overlay update race or viewport sync delay). - if (batchRenderedTargets.length === 0) return rawShipData; - return [...batchRenderedTargets]; - }, [hideLiveShips, batchRenderedTargets, rawShipData]); + if (shipData.length === 0) return shipData; + return [...shipData]; + }, [shipData]); const shipHighlightSet = useMemo(() => { const out = new Set(highlightedMmsiSetForShips); @@ -251,8 +236,6 @@ export function Map3D({ return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi)); }, [shipHighlightSet, shipLayerData]); - const trackReplayRenderState = useTrackReplayDeckLayers(); - // ── Deck hover management ──────────────────────────────────────────── const hasAuxiliarySelectModifier = useCallback( (ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null): boolean => { @@ -312,51 +295,22 @@ export function Map3D({ ownerKey: null, vesselMmsis: [], }); - const mapDrivenMmsiHoverRef = useRef(false); - const mapDrivenPairHoverRef = useRef(false); - const mapDrivenFleetHoverRef = useRef(false); const clearMapFleetHoverState = useCallback(() => { - const prev = mapFleetHoverStateRef.current; mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] }; setHoveredDeckFleetOwner(null); setHoveredDeckFleetMmsis([]); - if ( - mapDrivenFleetHoverRef.current && - (prev.ownerKey != null || prev.vesselMmsis.length > 0) && - hoveredFleetOwnerKey === prev.ownerKey && - equalNumberArrays(hoveredFleetMmsiSet, prev.vesselMmsis) - ) { - onClearFleetHover?.(); - } - mapDrivenFleetHoverRef.current = false; - }, [ - setHoveredDeckFleetOwner, - setHoveredDeckFleetMmsis, - onClearFleetHover, - hoveredFleetOwnerKey, - hoveredFleetMmsiSet, - ]); + }, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]); const clearDeckHoverPairs = useCallback(() => { - const prev = mapDeckPairHoverRef.current; mapDeckPairHoverRef.current = []; setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : [])); - if (mapDrivenPairHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredPairMmsiSet, prev)) { - onClearPairHover?.(); - } - mapDrivenPairHoverRef.current = false; - }, [setHoveredDeckPairMmsiSet, onClearPairHover, hoveredPairMmsiSet]); + }, [setHoveredDeckPairMmsiSet]); const clearDeckHoverMmsi = useCallback(() => { - const prev = mapDeckMmsiHoverRef.current; mapDeckMmsiHoverRef.current = []; setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : [])); - if (mapDrivenMmsiHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredMmsiSet, prev)) { - onClearMmsiHover?.(); - } - mapDrivenMmsiHoverRef.current = false; - }, [setHoveredDeckMmsiSet, onClearMmsiHover, hoveredMmsiSet]); + }, [setHoveredDeckMmsiSet]); const scheduleDeckHoverResolve = useCallback(() => { if (deckHoverRafRef.current != null) return; @@ -382,41 +336,21 @@ export function Map3D({ const setDeckHoverMmsi = useCallback( (next: number[]) => { const normalized = makeUniqueSorted(next); - const prev = mapDeckMmsiHoverRef.current; touchDeckHoverState(normalized.length > 0); setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized)); mapDeckMmsiHoverRef.current = normalized; - if (!equalNumberArrays(prev, normalized)) { - if (normalized.length > 0) { - mapDrivenMmsiHoverRef.current = true; - onHoverMmsi?.(normalized); - } else if (mapDrivenMmsiHoverRef.current && prev.length > 0) { - onClearMmsiHover?.(); - mapDrivenMmsiHoverRef.current = false; - } - } }, - [setHoveredDeckMmsiSet, touchDeckHoverState, onHoverMmsi, onClearMmsiHover], + [setHoveredDeckMmsiSet, touchDeckHoverState], ); const setDeckHoverPairs = useCallback( (next: number[]) => { const normalized = makeUniqueSorted(next); - const prev = mapDeckPairHoverRef.current; touchDeckHoverState(normalized.length > 0); setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized)); mapDeckPairHoverRef.current = normalized; - if (!equalNumberArrays(prev, normalized)) { - if (normalized.length > 0) { - mapDrivenPairHoverRef.current = true; - onHoverPair?.(normalized); - } else if (mapDrivenPairHoverRef.current && prev.length > 0) { - onClearPairHover?.(); - mapDrivenPairHoverRef.current = false; - } - } }, - [setHoveredDeckPairMmsiSet, touchDeckHoverState, onHoverPair, onClearPairHover], + [setHoveredDeckPairMmsiSet, touchDeckHoverState], ); const setMapFleetHoverState = useCallback( @@ -430,21 +364,8 @@ export function Map3D({ setHoveredDeckFleetOwner(ownerKey); setHoveredDeckFleetMmsis(normalized); mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized }; - if (ownerKey != null || normalized.length > 0) { - mapDrivenFleetHoverRef.current = true; - onHoverFleet?.(ownerKey, normalized); - } else if (mapDrivenFleetHoverRef.current) { - onClearFleetHover?.(); - mapDrivenFleetHoverRef.current = false; - } }, - [ - setHoveredDeckFleetOwner, - setHoveredDeckFleetMmsis, - touchDeckHoverState, - onHoverFleet, - onClearFleetHover, - ], + [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState], ); // hover RAF cleanup @@ -492,37 +413,37 @@ export function Map3D({ }, [pairLinks]); const pairLinksInteractive = useMemo(() => { - if ((pairLinks?.length ?? 0) === 0) return []; - if (effectiveHoveredPairMmsiSet.size < 2) return []; + if (!overlays.pairLines || (pairLinks?.length ?? 0) === 0) return []; + if (hoveredPairMmsiSetRef.size < 2) return []; const links = pairLinks || []; return links.filter((link) => - effectiveHoveredPairMmsiSet.has(link.aMmsi) && effectiveHoveredPairMmsiSet.has(link.bMmsi), + hoveredPairMmsiSetRef.has(link.aMmsi) && hoveredPairMmsiSetRef.has(link.bMmsi), ); - }, [pairLinks, effectiveHoveredPairMmsiSet]); + }, [pairLinks, hoveredPairMmsiSetRef, overlays.pairLines]); const pairRangesInteractive = useMemo(() => { - if (pairRanges.length === 0) return []; - if (effectiveHoveredPairMmsiSet.size < 2) return []; + if (!overlays.pairRange || pairRanges.length === 0) return []; + if (hoveredPairMmsiSetRef.size < 2) return []; return pairRanges.filter((range) => - effectiveHoveredPairMmsiSet.has(range.aMmsi) && effectiveHoveredPairMmsiSet.has(range.bMmsi), + hoveredPairMmsiSetRef.has(range.aMmsi) && hoveredPairMmsiSetRef.has(range.bMmsi), ); - }, [pairRanges, effectiveHoveredPairMmsiSet]); + }, [pairRanges, hoveredPairMmsiSetRef, overlays.pairRange]); const fcLinesInteractive = useMemo(() => { - if (fcDashed.length === 0) return []; + if (!overlays.fcLines || fcDashed.length === 0) return []; if (highlightedMmsiSetCombined.size === 0) return []; return fcDashed.filter( (line) => [line.fromMmsi, line.toMmsi].some((mmsi) => (mmsi == null ? false : highlightedMmsiSetCombined.has(mmsi))), ); - }, [fcDashed, highlightedMmsiSetCombined]); + }, [fcDashed, overlays.fcLines, highlightedMmsiSetCombined]); const fleetCirclesInteractive = useMemo(() => { - if ((fleetCircles?.length ?? 0) === 0) return []; + if (!overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) return []; if (hoveredFleetOwnerKeys.size === 0 && highlightedMmsiSetCombined.size === 0) return []; const circles = fleetCircles || []; return circles.filter((item) => isHighlightedFleet(item.ownerKey, item.vesselMmsis)); - }, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, highlightedMmsiSetCombined]); + }, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, overlays.fleetCircles, highlightedMmsiSetCombined]); // ── Hook orchestration ─────────────────────────────────────────────── const { ensureMercatorOverlay, pulseMapSync } = useMapInit( @@ -559,9 +480,9 @@ export function Map3D({ useGlobeShips( mapRef, projectionBusyRef, reorderGlobeFeatureLayers, { - projection, settings, shipData: shipLayerData, shipHighlightSet, shipHoverOverlaySet, + projection, settings, shipData, shipHighlightSet, shipHoverOverlaySet, shipOverlayLayerData, shipLayerData, shipByMmsi, mapSyncEpoch, - onSelectMmsi, onToggleHighlightMmsi, targets: shipLayerData, overlays, + onSelectMmsi, onToggleHighlightMmsi, targets, overlays, legacyHits, selectedMmsi, isBaseHighlightedMmsi, hasAuxiliarySelectModifier, onGlobeShipsReady, }, @@ -588,7 +509,7 @@ export function Map3D({ useDeckLayers( mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef, { - projection, settings, trackReplayDeckLayers: trackReplayRenderState.trackReplayDeckLayers, shipLayerData, shipOverlayLayerData, shipData, + projection, settings, trackReplayDeckLayers: [], shipLayerData, shipOverlayLayerData, shipData, legacyHits, pairLinks, fcLinks, fcDashed, fleetCircles, pairRanges, pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive, overlays, shipByMmsi, selectedMmsi, shipHighlightSet, @@ -615,9 +536,9 @@ export function Map3D({ }, ); - useTrackReplayLayer( - mapRef, projectionBusyRef, reorderGlobeFeatureLayers, - { activeTrack, projection, mapSyncEpoch, renderState: trackReplayRenderState }, + useVesselTrackLayer( + mapRef, overlayRef, projectionBusyRef, reorderGlobeFeatureLayers, + { activeTrack, projection, mapSyncEpoch }, ); // 우클릭 컨텍스트 메뉴 — 대상선박(legacyHits)만 허용 @@ -640,7 +561,7 @@ export function Map3D({ // Globe: MapLibre 네이티브 레이어에서 쿼리 const point: [number, number] = [e.offsetX, e.offsetY]; const shipLayerIds = [ - 'ships-globe', 'ships-globe-lite', 'ships-globe-halo', 'ships-globe-outline', + 'ships-globe', 'ships-globe-halo', 'ships-globe-outline', ].filter((id) => map.getLayer(id)); let features: maplibregl.MapGeoJSONFeature[] = []; diff --git a/apps/web/src/widgets/map3d/hooks/useGlobeOverlays.ts b/apps/web/src/widgets/map3d/hooks/useGlobeOverlays.ts index b2261d6..db3768b 100644 --- a/apps/web/src/widgets/map3d/hooks/useGlobeOverlays.ts +++ b/apps/web/src/widgets/map3d/hooks/useGlobeOverlays.ts @@ -11,7 +11,6 @@ import { PAIR_RANGE_NORMAL_ML_HL, PAIR_RANGE_WARN_ML_HL, FC_LINE_NORMAL_ML, FC_LINE_SUSPICIOUS_ML, FC_LINE_NORMAL_ML_HL, FC_LINE_SUSPICIOUS_ML_HL, - FLEET_FILL_ML_HL, FLEET_LINE_ML, FLEET_LINE_ML_HL, } from '../constants'; import { makeUniqueSorted } from '../lib/setUtils'; @@ -67,8 +66,7 @@ export function useGlobeOverlays( const ensure = () => { if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; - const pairHoverActive = hoveredPairMmsiList.length >= 2; - if (projection !== 'globe' || (!overlays.pairLines && !pairHoverActive) || (pairLinks?.length ?? 0) === 0) { + if (projection !== 'globe' || !overlays.pairLines || (pairLinks?.length ?? 0) === 0) { remove(); return; } @@ -142,7 +140,7 @@ export function useGlobeOverlays( return () => { stop(); }; - }, [projection, overlays.pairLines, pairLinks, hoveredPairMmsiList, mapSyncEpoch, reorderGlobeFeatureLayers]); + }, [projection, overlays.pairLines, pairLinks, mapSyncEpoch, reorderGlobeFeatureLayers]); // FC lines useEffect(() => { @@ -159,9 +157,7 @@ export function useGlobeOverlays( const ensure = () => { if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; - const fleetAwarePairMmsiList = makeUniqueSorted([...hoveredPairMmsiList, ...hoveredFleetMmsiList]); - const fcHoverActive = fleetAwarePairMmsiList.length > 0; - if (projection !== 'globe' || (!overlays.fcLines && !fcHoverActive)) { + if (projection !== 'globe' || !overlays.fcLines) { remove(); return; } @@ -239,15 +235,7 @@ export function useGlobeOverlays( return () => { stop(); }; - }, [ - projection, - overlays.fcLines, - fcLinks, - hoveredPairMmsiList, - hoveredFleetMmsiList, - mapSyncEpoch, - reorderGlobeFeatureLayers, - ]); + }, [projection, overlays.fcLines, fcLinks, mapSyncEpoch, reorderGlobeFeatureLayers]); // Fleet circles useEffect(() => { @@ -255,35 +243,26 @@ export function useGlobeOverlays( if (!map) return; const srcId = 'fleet-circles-ml-src'; - const fillSrcId = 'fleet-circles-ml-fill-src'; const layerId = 'fleet-circles-ml'; - const fillLayerId = 'fleet-circles-ml-fill'; // fill layer 제거됨 — globe tessellation에서 vertex 65535 초과 경고 원인 // 라인만으로 fleet circle 시각화 충분 const remove = () => { guardedSetVisibility(map, layerId, 'none'); - guardedSetVisibility(map, fillLayerId, 'none'); }; const ensure = () => { if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; - const fleetHoverActive = hoveredFleetOwnerKeyList.length > 0 || hoveredFleetMmsiList.length > 0; - if (projection !== 'globe' || (!overlays.fleetCircles && !fleetHoverActive) || (fleetCircles?.length ?? 0) === 0) { + if (projection !== 'globe' || !overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) { remove(); return; } - const circles = fleetCircles || []; - const isHighlightedFleet = (ownerKey: string, vesselMmsis: number[]) => - hoveredFleetOwnerKeyList.includes(ownerKey) || - (hoveredFleetMmsiList.length > 0 && vesselMmsis.some((mmsi) => hoveredFleetMmsiList.includes(mmsi))); - const fcLine: GeoJSON.FeatureCollection = { type: 'FeatureCollection', - features: circles.map((c) => { + features: (fleetCircles || []).map((c) => { const ring = circleRingLngLat(c.center, c.radiusNm * 1852); return { type: 'Feature', @@ -301,23 +280,6 @@ export function useGlobeOverlays( }), }; - const fcFill: GeoJSON.FeatureCollection = { - type: 'FeatureCollection', - features: circles - .filter((c) => isHighlightedFleet(c.ownerKey, c.vesselMmsis)) - .map((c) => ({ - type: 'Feature', - id: makeFleetCircleFeatureId(`${c.ownerKey}-fill`), - geometry: { - type: 'Polygon', - coordinates: [circleRingLngLat(c.center, c.radiusNm * 1852, 24)], - }, - properties: { - ownerKey: c.ownerKey, - }, - })), - }; - try { const existing = map.getSource(srcId) as GeoJSONSource | undefined; if (existing) existing.setData(fcLine); @@ -327,14 +289,6 @@ export function useGlobeOverlays( return; } - try { - const existingFill = map.getSource(fillSrcId) as GeoJSONSource | undefined; - if (existingFill) existingFill.setData(fcFill); - else map.addSource(fillSrcId, { type: 'geojson', data: fcFill } as GeoJSONSourceSpecification); - } catch (e) { - console.warn('Fleet circles fill source setup failed:', e); - } - if (!map.getLayer(layerId)) { try { map.addLayer( @@ -358,27 +312,6 @@ export function useGlobeOverlays( guardedSetVisibility(map, layerId, 'visible'); } - if (!map.getLayer(fillLayerId)) { - try { - map.addLayer( - { - id: fillLayerId, - type: 'fill', - source: fillSrcId, - layout: { visibility: fcFill.features.length > 0 ? 'visible' : 'none' }, - paint: { - 'fill-color': FLEET_FILL_ML_HL, - }, - } as unknown as LayerSpecification, - undefined, - ); - } catch (e) { - console.warn('Fleet circles fill layer add failed:', e); - } - } else { - guardedSetVisibility(map, fillLayerId, fcFill.features.length > 0 ? 'visible' : 'none'); - } - reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -388,15 +321,7 @@ export function useGlobeOverlays( return () => { stop(); }; - }, [ - projection, - overlays.fleetCircles, - fleetCircles, - hoveredFleetOwnerKeyList, - hoveredFleetMmsiList, - mapSyncEpoch, - reorderGlobeFeatureLayers, - ]); + }, [projection, overlays.fleetCircles, fleetCircles, mapSyncEpoch, reorderGlobeFeatureLayers]); // Pair range useEffect(() => { @@ -413,8 +338,7 @@ export function useGlobeOverlays( const ensure = () => { if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; - const pairHoverActive = hoveredPairMmsiList.length >= 2; - if (projection !== 'globe' || (!overlays.pairRange && !pairHoverActive)) { + if (projection !== 'globe' || !overlays.pairRange) { remove(); return; } @@ -503,7 +427,7 @@ export function useGlobeOverlays( return () => { stop(); }; - }, [projection, overlays.pairRange, pairLinks, hoveredPairMmsiList, mapSyncEpoch, reorderGlobeFeatureLayers]); + }, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, reorderGlobeFeatureLayers]); // Paint state updates for hover highlights // eslint-disable-next-line react-hooks/preserve-manual-memoization diff --git a/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts b/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts index 8cbc4ee..5d55bdb 100644 --- a/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts +++ b/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts @@ -270,14 +270,13 @@ export function useGlobeShips( const srcId = 'ships-globe-src'; const haloId = 'ships-globe-halo'; const outlineId = 'ships-globe-outline'; - const symbolLiteId = 'ships-globe-lite'; const symbolId = 'ships-globe'; const labelId = 'ships-globe-label'; // 레이어를 제거하지 않고 visibility만 'none'으로 설정 // guardedSetVisibility로 현재 값과 동일하면 호출 생략 (style._changed 방지) const hide = () => { - for (const id of [labelId, symbolId, symbolLiteId, outlineId, haloId]) { + for (const id of [labelId, symbolId, outlineId, haloId]) { guardedSetVisibility(map, id, 'none'); } }; @@ -301,12 +300,10 @@ export function useGlobeShips( // → style._changed 방지 → 불필요한 symbol placement 재계산 방지 → 라벨 사라짐 방지 const visibility: 'visible' | 'none' = projection === 'globe' ? 'visible' : 'none'; const labelVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none'; - if (map.getLayer(symbolId) || map.getLayer(symbolLiteId)) { - const changed = - map.getLayoutProperty(symbolId, 'visibility') !== visibility || - map.getLayoutProperty(symbolLiteId, 'visibility') !== visibility; + if (map.getLayer(symbolId)) { + const changed = map.getLayoutProperty(symbolId, 'visibility') !== visibility; if (changed) { - for (const id of [haloId, outlineId, symbolLiteId, symbolId]) { + for (const id of [haloId, outlineId, symbolId]) { guardedSetVisibility(map, id, visibility); } if (projection === 'globe') kickRepaint(map); @@ -345,18 +342,6 @@ export function useGlobeShips( } const before = undefined; - const priorityFilter = [ - 'any', - ['==', ['to-number', ['get', 'permitted'], 0], 1], - ['==', ['to-number', ['get', 'selected'], 0], 1], - ['==', ['to-number', ['get', 'highlighted'], 0], 1], - ] as unknown as unknown[]; - const nonPriorityFilter = [ - 'all', - ['==', ['to-number', ['get', 'permitted'], 0], 0], - ['==', ['to-number', ['get', 'selected'], 0], 0], - ['==', ['to-number', ['get', 'highlighted'], 0], 0], - ] as unknown as unknown[]; if (!map.getLayer(haloId)) { try { @@ -443,76 +428,6 @@ export function useGlobeShips( } // outline: data-driven expressions are static — visibility handled by fast toggle - if (!map.getLayer(symbolLiteId)) { - try { - map.addLayer( - { - id: symbolLiteId, - type: 'symbol', - source: srcId, - minzoom: 6.5, - filter: nonPriorityFilter as never, - layout: { - visibility, - 'symbol-sort-key': 40 as never, - 'icon-image': [ - 'case', - ['==', ['to-number', ['get', 'isAnchored'], 0], 1], - anchoredImgId, - imgId, - ] as never, - 'icon-size': [ - 'interpolate', - ['linear'], - ['zoom'], - 6.5, - ['*', ['to-number', ['get', 'iconSize7'], 0.45], 0.45], - 8, - ['*', ['to-number', ['get', 'iconSize7'], 0.45], 0.62], - 10, - ['*', ['to-number', ['get', 'iconSize10'], 0.58], 0.72], - 14, - ['*', ['to-number', ['get', 'iconSize14'], 0.85], 0.78], - 18, - ['*', ['to-number', ['get', 'iconSize18'], 2.5], 0.78], - ] as unknown as number[], - 'icon-allow-overlap': true, - 'icon-ignore-placement': true, - 'icon-anchor': 'center', - 'icon-rotate': [ - 'case', - ['==', ['to-number', ['get', 'isAnchored'], 0], 1], - 0, - ['to-number', ['get', 'heading'], 0], - ] as never, - 'icon-rotation-alignment': 'map', - 'icon-pitch-alignment': 'map', - }, - paint: { - 'icon-color': ['coalesce', ['get', 'shipColor'], '#64748b'] as never, - 'icon-opacity': [ - 'interpolate', - ['linear'], - ['zoom'], - 6.5, - 0.16, - 8, - 0.34, - 11, - 0.54, - 14, - 0.68, - ] as never, - }, - } as unknown as LayerSpecification, - before, - ); - } catch (e) { - console.warn('Ship lite symbol layer add failed:', e); - } - } - // lite symbol: lower LOD for non-priority vessels in low zoom - if (!map.getLayer(symbolId)) { try { map.addLayer( @@ -520,7 +435,6 @@ export function useGlobeShips( id: symbolId, type: 'symbol', source: srcId, - filter: priorityFilter as never, layout: { visibility, 'symbol-sort-key': [ @@ -561,10 +475,10 @@ export function useGlobeShips( 'icon-color': ['coalesce', ['get', 'shipColor'], '#64748b'] as never, 'icon-opacity': [ 'case', - ['==', ['get', 'selected'], 1], 1, - ['==', ['get', 'highlighted'], 1], 0.95, - ['==', ['get', 'permitted'], 1], 0.93, - 0.9, + ['==', ['get', 'permitted'], 1], 1, + ['==', ['get', 'selected'], 1], 0.86, + ['==', ['get', 'highlighted'], 1], 0.82, + 0.66, ] as never, }, } as unknown as LayerSpecification, @@ -906,14 +820,13 @@ export function useGlobeShips( if (projection !== 'globe' || !settings.showShips) return; const symbolId = 'ships-globe'; - const symbolLiteId = 'ships-globe-lite'; const haloId = 'ships-globe-halo'; const outlineId = 'ships-globe-outline'; const clickedRadiusDeg2 = Math.pow(0.08, 2); const onClick = (e: maplibregl.MapMouseEvent) => { try { - const layerIds = [symbolId, symbolLiteId, haloId, outlineId].filter((id) => map.getLayer(id)); + const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id)); let feats: unknown[] = []; if (layerIds.length > 0) { try { diff --git a/apps/web/src/widgets/map3d/hooks/useMapInit.ts b/apps/web/src/widgets/map3d/hooks/useMapInit.ts index 5875d89..0a276fd 100644 --- a/apps/web/src/widgets/map3d/hooks/useMapInit.ts +++ b/apps/web/src/widgets/map3d/hooks/useMapInit.ts @@ -151,7 +151,6 @@ export function useMapInit( } mapRef.current = map; - setMapSyncEpoch((prev) => prev + 1); // 양쪽 overlay를 모두 초기화 — projection 전환 시 재생성 비용 제거 ensureMercatorOverlay(); diff --git a/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts b/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts index c38cc38..4b3e3cc 100644 --- a/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts +++ b/apps/web/src/widgets/map3d/hooks/useProjectionToggle.ts @@ -99,10 +99,6 @@ export function useProjectionToggle( 'vessel-track-arrow', 'vessel-track-pts', 'vessel-track-pts-highlight', - 'track-replay-globe-path', - 'track-replay-globe-points', - 'track-replay-globe-virtual-ship', - 'track-replay-globe-virtual-label', 'zones-fill', 'zones-line', 'zones-label', @@ -112,7 +108,6 @@ export function useProjectionToggle( 'predict-vectors-hl', 'ships-globe-halo', 'ships-globe-outline', - 'ships-globe-lite', 'ships-globe', 'ships-globe-label', 'ships-globe-hover-halo', @@ -121,7 +116,6 @@ export function useProjectionToggle( 'pair-lines-ml', 'fc-lines-ml', 'pair-range-ml', - 'fleet-circles-ml-fill', 'fleet-circles-ml', ]; diff --git a/apps/web/src/widgets/map3d/hooks/useVesselTrackLayer.ts b/apps/web/src/widgets/map3d/hooks/useVesselTrackLayer.ts index ce734eb..e46bc6f 100644 --- a/apps/web/src/widgets/map3d/hooks/useVesselTrackLayer.ts +++ b/apps/web/src/widgets/map3d/hooks/useVesselTrackLayer.ts @@ -107,7 +107,6 @@ const GLOBE_LAYERS: NativeLayerSpec[] = [ const ANIM_CYCLE_SEC = 20; /* ── Hook ──────────────────────────────────────────────────────────── */ -/** @deprecated trackReplay store 엔진으로 이관 완료. 유지보수 호환 용도로만 남겨둔다. */ export function useVesselTrackLayer( mapRef: MutableRefObject, overlayRef: MutableRefObject, diff --git a/package-lock.json b/package-lock.json index 28a7738..84a7523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1406,18 +1406,6 @@ "@loaders.gl/core": "^4.3.0" } }, - "node_modules/@loaders.gl/compression/node_modules/fflate": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", - "license": "MIT" - }, - "node_modules/@loaders.gl/compression/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/@loaders.gl/core": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz", @@ -3981,9 +3969,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", "license": "MIT" }, "node_modules/file-entry-cache": { @@ -4111,6 +4099,12 @@ "node": ">=10.19" } }, + "node_modules/geotiff/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/geotiff/node_modules/quick-lru": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", @@ -4466,12 +4460,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/jszip/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/kdbush": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", @@ -4752,6 +4740,12 @@ "fflate": "^0.8.0" } }, + "node_modules/numcodecs/node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/ol": { "version": "10.8.0", "resolved": "https://registry.npmjs.org/ol/-/ol-10.8.0.tgz", @@ -4836,9 +4830,9 @@ } }, "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/parent-module": {