/** * 구역 그리기 OpenLayers Draw 인터랙션 훅 * * - activeDrawType 변경 시 Draw 인터랙션 활성화 * - Polygon / Box / Circle 그리기 * - drawend → EPSG:3857→4326 변환 → addZone() * - ESC 키로 그리기 취소 * - 구역별 색상 스타일 (ZONE_COLORS) */ import { useEffect, useRef, useCallback } from 'react'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import { Draw } from 'ol/interaction'; import { createBox } from 'ol/interaction/Draw'; import { Style, Fill, Stroke } from 'ol/style'; import { transform } from 'ol/proj'; import { fromCircle } from 'ol/geom/Polygon'; import { useMapStore } from '../../stores/mapStore'; import { useAreaSearchStore } from '../stores/areaSearchStore'; import { ZONE_COLORS, ZONE_DRAW_TYPES } from '../types/areaSearch.types'; import { setZoneSource, getZoneSource, setZoneLayer, getZoneLayer } from '../utils/zoneLayerRefs'; /** * 3857 좌표를 4326 좌표로 변환하고 폐곡선 보장 */ function toWgs84Polygon(coords3857) { const coords4326 = coords3857.map((c) => transform(c, 'EPSG:3857', 'EPSG:4326')); // 폐곡선 보장 (첫점 == 끝점) if (coords4326.length > 0) { const first = coords4326[0]; const last = coords4326[coords4326.length - 1]; if (first[0] !== last[0] || first[1] !== last[1]) { coords4326.push([...first]); } } return coords4326; } /** * 구역 인덱스에 맞는 OL 스타일 생성 */ function createZoneStyle(index) { const color = ZONE_COLORS[index] || ZONE_COLORS[0]; return new Style({ fill: new Fill({ color: `rgba(${color.fill.join(',')})` }), stroke: new Stroke({ color: `rgba(${color.stroke.join(',')})`, width: 2 }), }); } export default function useZoneDraw() { const map = useMapStore((s) => s.map); const drawRef = useRef(null); const mapRef = useRef(null); // map ref 동기화 (클린업에서 사용) useEffect(() => { mapRef.current = map; }, [map]); // 맵 준비 시 레이어 설정 useEffect(() => { if (!map) return; const source = new VectorSource({ wrapX: false }); const layer = new VectorLayer({ source, zIndex: 55, }); map.addLayer(layer); setZoneSource(source); setZoneLayer(layer); // 기존 zones가 있으면 동기화 const { zones } = useAreaSearchStore.getState(); zones.forEach((zone) => { if (!zone.olFeature) return; zone.olFeature.setStyle(createZoneStyle(zone.colorIndex)); source.addFeature(zone.olFeature); }); return () => { if (drawRef.current) { map.removeInteraction(drawRef.current); drawRef.current = null; } map.removeLayer(layer); setZoneSource(null); setZoneLayer(null); }; }, [map]); // 스토어의 zones 변경 → OL feature 동기화 useEffect(() => { const unsub = useAreaSearchStore.subscribe( (s) => s.zones, (zones) => { const source = getZoneSource(); if (!source) return; source.clear(); zones.forEach((zone) => { if (!zone.olFeature) return; zone.olFeature.setStyle(createZoneStyle(zone.colorIndex)); source.addFeature(zone.olFeature); }); }, ); return unsub; }, []); // showZones 변경 → 레이어 표시/숨김 useEffect(() => { const unsub = useAreaSearchStore.subscribe( (s) => s.showZones, (show) => { const layer = getZoneLayer(); if (layer) layer.setVisible(show); }, ); return unsub; }, []); // Draw 인터랙션 생성 함수 const setupDraw = useCallback((currentMap, drawType) => { // 기존 인터랙션 제거 if (drawRef.current) { currentMap.removeInteraction(drawRef.current); drawRef.current = null; } if (!drawType) return; const source = getZoneSource(); if (!source) return; // source를 Draw에 전달하지 않음 // OL Draw는 drawend 이벤트 후에 source.addFeature()를 자동 호출하는데, // 우리 drawend 핸들러의 addZone() → subscription → source.addFeature() 와 충돌하여 // "feature already added" 에러 발생. source를 제거하면 자동 추가가 비활성화됨. let draw; if (drawType === ZONE_DRAW_TYPES.BOX) { draw = new Draw({ type: 'Circle', geometryFunction: createBox() }); } else if (drawType === ZONE_DRAW_TYPES.CIRCLE) { draw = new Draw({ type: 'Circle' }); } else { draw = new Draw({ type: 'Polygon' }); } draw.on('drawend', (evt) => { const feature = evt.feature; let geom = feature.getGeometry(); const typeName = drawType; // Circle → Polygon 변환 (center/radius 보존) let circleMeta = null; if (drawType === ZONE_DRAW_TYPES.CIRCLE) { circleMeta = { center: geom.getCenter(), radius: geom.getRadius() }; const polyGeom = fromCircle(geom, 64); feature.setGeometry(polyGeom); geom = polyGeom; } // EPSG:3857 → 4326 좌표 추출 const coords3857 = geom.getCoordinates()[0]; const coordinates = toWgs84Polygon(coords3857); // 최소 4점 확인 if (coordinates.length < 4) { return; } const { zones } = useAreaSearchStore.getState(); const index = zones.length; const style = createZoneStyle(index); feature.setStyle(style); // source에 직접 추가 (즉시 표시, Draw의 자동 추가를 대체) source.addFeature(feature); // 상태 업데이트를 다음 틱으로 지연 // drawend 이벤트 처리 중에 Draw를 동기적으로 제거하면, // OL 내부 이벤트 체인이 완료되기 전에 DragPan이 이벤트를 가로채서 // 지도가 마우스를 따라 움직이는 문제가 발생함. // setTimeout으로 OL 이벤트 처리가 완료된 후 안전하게 제거. setTimeout(() => { useAreaSearchStore.getState().addZone({ type: typeName, source: 'draw', coordinates, olFeature: feature, circleMeta, }); // addZone → activeDrawType: null → subscription → removeInteraction }, 0); }); currentMap.addInteraction(draw); drawRef.current = draw; }, []); // activeDrawType 변경 → Draw 인터랙션 설정 useEffect(() => { if (!map) return; const unsub = useAreaSearchStore.subscribe( (s) => s.activeDrawType, (drawType) => { setupDraw(map, drawType); }, ); // 현재 activeDrawType이 이미 설정되어 있으면 즉시 적용 const { activeDrawType } = useAreaSearchStore.getState(); if (activeDrawType) { setupDraw(map, activeDrawType); } return () => { unsub(); // 구독 해제 시 Draw 인터랙션도 제거 if (drawRef.current && mapRef.current) { mapRef.current.removeInteraction(drawRef.current); drawRef.current = null; } }; }, [map, setupDraw]); // ESC 키로 그리기 취소 useEffect(() => { const handleKeyDown = (e) => { if (e.key === 'Escape') { const { activeDrawType } = useAreaSearchStore.getState(); if (activeDrawType) { useAreaSearchStore.getState().setActiveDrawType(null); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); // 구역 삭제 시 OL feature도 source에서 제거 (zones 감소) useEffect(() => { const unsub = useAreaSearchStore.subscribe( (s) => s.zones, (zones, prevZones) => { if (!prevZones || zones.length >= prevZones.length) return; const source = getZoneSource(); if (!source) return; const currentIds = new Set(zones.map((z) => z.id)); prevZones.forEach((z) => { if (!currentIds.has(z.id) && z.olFeature) { try { source.removeFeature(z.olFeature); } catch { /* already removed */ } } }); }, ); return unsub; }, []); }