From d5700ba5873baec270d649db934029909e607004 Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 16 Feb 2026 13:59:21 +0900 Subject: [PATCH] =?UTF-8?q?fix(map):=20zone=20=EA=B0=84=EC=86=8C=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20projectionBusy=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 소스 데이터 간소화가 projectionBusy 가드 뒤에 있어서 globe 전환 시 원본 데이터(2100+ vertex)로 tessellation 진행 → 73,000+ vertex 폭증. setData를 가드 앞으로 이동하고 useMemo로 간소화 데이터 캐싱. Co-Authored-By: Claude Opus 4.6 --- .../src/widgets/map3d/hooks/useZonesLayer.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/web/src/widgets/map3d/hooks/useZonesLayer.ts b/apps/web/src/widgets/map3d/hooks/useZonesLayer.ts index 6215356..ea1f29e 100644 --- a/apps/web/src/widgets/map3d/hooks/useZonesLayer.ts +++ b/apps/web/src/widgets/map3d/hooks/useZonesLayer.ts @@ -1,4 +1,4 @@ -import { useEffect, type MutableRefObject } from 'react'; +import { useEffect, useMemo, type MutableRefObject } from 'react'; import maplibregl, { type GeoJSONSource, type GeoJSONSourceSpecification, @@ -54,6 +54,12 @@ export function useZonesLayer( ) { const { zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch } = opts; + // globe용 간소화 데이터를 미리 캐싱 — ensure() 내 매번 재계산 방지 + const simplifiedZones = useMemo( + () => (zones ? simplifyZonesForGlobe(zones) : null), + [zones], + ); + useEffect(() => { const map = mapRef.current; if (!map) return; @@ -75,26 +81,32 @@ export function useZonesLayer( zoneLabelExpr.push(['coalesce', ['get', 'zoneName'], ['get', 'zoneLabel'], ['get', 'NAME'], '수역']); const ensure = () => { - if (projectionBusyRef.current) return; + // 소스 데이터 간소화 — projectionBusy 중에도 실행해야 함 + // globe 전환 시 projectionBusy 가드 뒤에서만 실행하면 MapLibre가 + // 원본(2100+ vertex) 데이터로 globe tessellation → 73,000+ vertex → 노란 막대 + const sourceData = projection === 'globe' ? simplifiedZones : zones; + if (sourceData) { + try { + const existing = map.getSource(srcId) as GeoJSONSource | undefined; + if (existing) existing.setData(sourceData); + } catch { /* ignore — source may not exist yet */ } + } + const visibility: 'visible' | 'none' = overlays.zones ? 'visible' : 'none'; - // globe 모드에서 fill polygon은 tessellation으로 vertex 65535 초과 → 숨김 - // (해안선 디테일 2100+ vertex가 globe에서 100,000+로 폭증하여 노란 막대 아티팩트 발생) const fillVisibility: 'visible' | 'none' = projection === 'globe' ? 'none' : visibility; guardedSetVisibility(map, fillId, fillVisibility); guardedSetVisibility(map, lineId, visibility); guardedSetVisibility(map, labelId, visibility); + if (projectionBusyRef.current) return; if (!zones) return; if (!map.isStyleLoaded()) return; try { - // globe: 서브샘플링된 데이터로 vertex 폭증 방지, mercator: 원본 데이터 - const sourceData = projection === 'globe' ? simplifyZonesForGlobe(zones) : zones; - const existing = map.getSource(srcId) as GeoJSONSource | undefined; - if (existing) { - existing.setData(sourceData); - } else { - map.addSource(srcId, { type: 'geojson', data: sourceData } as GeoJSONSourceSpecification); + // 소스가 아직 없으면 생성 (setData는 위에서 이미 처리됨) + if (!map.getSource(srcId)) { + const data = projection === 'globe' ? simplifiedZones ?? zones : zones; + map.addSource(srcId, { type: 'geojson', data: data! } as GeoJSONSourceSpecification); } const style = map.getStyle(); @@ -247,5 +259,5 @@ export function useZonesLayer( return () => { stop(); }; - }, [zones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch, reorderGlobeFeatureLayers]); + }, [zones, simplifiedZones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch, reorderGlobeFeatureLayers]); }