/** * useNativeMapLayers — Mercator/Globe 공통 MapLibre 네이티브 레이어 관리 hook * * 반복되는 보일러플레이트를 자동화합니다: * - projectionBusy / isStyleLoaded 가드 * - GeoJSON source 생성/업데이트 * - Layer 생성 (ensureLayer) * - Visibility 토글 * - Globe 레이어 순서 관리 (reorderGlobeFeatureLayers) * - kickRepaint * - Unmount 시 cleanupLayers * * 호버 하이라이트, 마우스 이벤트 등 레이어별 커스텀 로직은 * 별도 useEffect에서 처리합니다. */ import { useEffect, useRef, type MutableRefObject } from 'react'; import maplibregl, { type GeoJSONSourceSpecification, type LayerSpecification } from 'maplibre-gl'; import { ensureGeoJsonSource, ensureLayer, setLayerVisibility, cleanupLayers } from '../lib/layerHelpers'; import { kickRepaint, onMapStyleReady } from '../lib/mapCore'; /* ── Public types ──────────────────────────────────────────────────── */ export interface NativeSourceConfig { id: string; data: GeoJSON.GeoJSON | null; /** GeoJSON source 옵션 (tolerance, buffer 등) */ options?: Partial>; } export interface NativeLayerSpec { id: string; type: 'line' | 'fill' | 'circle' | 'symbol'; sourceId: string; paint: Record; layout?: Record; filter?: unknown[]; minzoom?: number; maxzoom?: number; } export interface NativeMapLayersConfig { /** GeoJSON 데이터 소스 (다중 지원) */ sources: NativeSourceConfig[]; /** 레이어 스펙 배열 (생성 순서대로) */ layers: NativeLayerSpec[]; /** 전체 레이어 on/off */ visible: boolean; /** * 이 레이어들을 삽입할 기준 레이어 ID. * 배열이면 첫 번째로 존재하는 레이어를 사용합니다. */ beforeLayer?: string | string[]; /** * 레이어 (재)생성 후 호출되는 콜백. * 호버 하이라이트 재적용 등에 사용합니다. */ onAfterSetup?: (map: maplibregl.Map) => void; } /* ── Hook ──────────────────────────────────────────────────────────── */ /** * @param mapRef - Map 인스턴스 ref * @param projectionBusyRef - 프로젝션 전환 중 가드 ref * @param reorderGlobeFeatureLayers - Globe 레이어 순서 재정렬 함수 * @param config - 소스/레이어/visibility 설정 * @param deps - 이 값이 변경되면 레이어를 다시 셋업합니다. * (subcableGeo, overlays.subcables, projection, mapSyncEpoch 등) */ export function useNativeMapLayers( mapRef: MutableRefObject, projectionBusyRef: MutableRefObject, reorderGlobeFeatureLayers: () => void, config: NativeMapLayersConfig, deps: readonly unknown[], ) { // 최신 config를 항상 읽기 위한 ref (deps에 config 객체를 넣지 않기 위함) const configRef = useRef(config); useEffect(() => { configRef.current = config; }); /* ── 레이어 생성/데이터 업데이트 ─────────────────────────────────── */ useEffect(() => { const map = mapRef.current; if (!map) return; const ensure = () => { const cfg = configRef.current; if (projectionBusyRef.current) return; // 1. Visibility 토글 for (const spec of cfg.layers) { setLayerVisibility(map, spec.id, cfg.visible); } // 2. 데이터가 있는 source가 하나도 없으면 종료 const hasData = cfg.sources.some((s) => s.data != null); if (!hasData) return; if (!map.isStyleLoaded()) return; try { // 3. Source 생성/업데이트 for (const src of cfg.sources) { if (src.data) { ensureGeoJsonSource(map, src.id, src.data, src.options); } } // 4. Before layer 해석 let before: string | undefined; if (cfg.beforeLayer) { const candidates = Array.isArray(cfg.beforeLayer) ? cfg.beforeLayer : [cfg.beforeLayer]; for (const candidate of candidates) { if (map.getLayer(candidate)) { before = candidate; break; } } } // 5. Layer 생성 const vis = cfg.visible ? 'visible' : 'none'; for (const spec of cfg.layers) { const layerDef: Record = { id: spec.id, type: spec.type, source: spec.sourceId, paint: spec.paint, layout: { ...spec.layout, visibility: vis }, }; if (spec.filter) layerDef.filter = spec.filter; if (spec.minzoom != null) layerDef.minzoom = spec.minzoom; if (spec.maxzoom != null) layerDef.maxzoom = spec.maxzoom; ensureLayer(map, layerDef as unknown as LayerSpecification, { before }); } // 6. Post-setup callback if (cfg.onAfterSetup) { cfg.onAfterSetup(map); } } catch (e) { console.warn('Native map layers setup failed:', e); } finally { reorderGlobeFeatureLayers(); kickRepaint(map); } }; const stop = onMapStyleReady(map, ensure); return () => { stop(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); /* ── Unmount cleanup ─────────────────────────────────────────────── */ useEffect(() => { const mapInstance = mapRef.current; return () => { if (!mapInstance) return; const cfg = configRef.current; const layerIds = [...cfg.layers].reverse().map((l) => l.id); const sourceIds = [...cfg.sources].reverse().map((s) => s.id); cleanupLayers(mapInstance, layerIds, sourceIds); }; }, []); }