"use client"; import Gis from "@common/business_components/ui/map/Gis"; import { useEffect, useRef, useState, useCallback } from "react"; import { mapService } from "../../service"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import { SearchBar } from "@common/business_components/ui/map/searchBar/searchBar"; import { useWrStore } from "@/app/store"; import { featurePopUpOpen, getClickFeatureId, getFeatureCd, getSelectedLayers, getWrFeaturePk, getWrLayerNm, } from "@/components/lib/func/mapUtil"; import toast, { Toaster } from "react-hot-toast"; import createOverlayManager from "@common/business_components/ui/map/GisOverlay"; import { createRoot } from "react-dom/client"; import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay"; const Map = () => { const {code, comMap ,rainMapDetile ,rswtMapDetile,getRuseMaplListReturn,pbnMapDetaile,ldrmntMapDetile,htdfMapDetile,coSttnMapDetail} = useWrStore((state) => state); const { isFeatured } = comMap; const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const gisMaps = useRef(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 const [checkedLayers, setCheckedLayers] = useState([]); const [checkedLayersData, setCheckedLayersData] = useState([]); const popupRef = useRef(null); const popupDetailRef = useRef(null); const fetchData = async () => { try { const response = await mapService.getLyrStngs("WR_NETWORK"); setTreeData(response); // API 응답 저장 } catch (error) { console.error("데이터 불러오기 실패:", error); } }; // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 // const handlePopupClose = useCallback(() => { // setIsOpen(false); // setFeatureData(null); // }, []); // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); useEffect(() => { const messageHandler = (event: MessageEvent) => { if (event.data.type === "POPUP_CLOSED") { if (event.data.target === "detail") { setFeatureData(null); setIsOpen(false); setIsSearchModalOpen(true); popupDetailRef.current = null; } else { popupRef.current = null; } } }; window.addEventListener("message", messageHandler); return () => { window.removeEventListener("message", messageHandler); }; }, []); // 체크된 레이어 정보를 받는 핸들러 const handleCheckedLayersChange = ( checkedIds: string[], checkedNodesData: any[] ) => { setCheckedLayers(checkedIds); setCheckedLayersData(checkedNodesData); }; // SearchModal, GisContent 등에서 호출 const onFeatureFocus = (data: any) => { console.log("data", data); }; const onFeatureSelect = (data: any, popUpClick?: boolean) => { const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); return; } // 항상 같은 select 객체 사용 const select = GisApp.Module.select as any; if (!select) return; // 1. 기존 선택 초기화 select.resetSelectedFeature(); // 2. data가 OpenLayers Feature 인스턴스인지 확인 let foundFeature: any = null; let featureId = data.fid; if (popUpClick) { const layerName = getWrLayerNm(data); const featurePk = getWrFeaturePk(layerName ?? ""); // 재이용시설 if (featurePk) { const lastPk = layerName === "tb_wr_ruse_fclt_dmd_co" ? data[featurePk].replaceAll("-", "") : data[featurePk]; featureId = `${layerName}.${lastPk}`; } else { console.warn("해당 피처는 선택된 레이어에서 찾을수 없습니다."); } } else { featureId = getClickFeatureId(data); } map .getLayers() .getArray() .forEach((layer: any) => { const source = layer.getSource?.(); if (source?.getFeatureById) { const feature = source.getFeatureById(String(featureId).trim()); if (feature) foundFeature = feature; } }); // 3. 피처 선택 if (foundFeature) { const geometry = foundFeature.getGeometry(); if (geometry) { const extent = geometry.getExtent(); map.getView().fit(extent, { duration: 300, padding: [50, 50, 50, 50], maxZoom: 16, constrainResolution: true, nearest: true, }); } select.selectFeature(foundFeature, true); } else { // toast.error('위치정보를 찾을수 없습니다') console.warn("OpenLayers Feature를 찾을 수 없습니다:"); console.log("foundFeature :", foundFeature); console.warn("featureId : ", featureId); console.warn("data : ", data); console.warn(": 끝"); } const currentId = data?.fid || data?.id || data?.ftrIdn; const lastId = featureData?.fid || featureData?.id || featureData?.ftrIdn; if (featureData && currentId === lastId) return; // 이전과 같으면 무시 setFeatureData(data); setIsOpen(true); setIsSearchModalOpen(false); if ( popUpClick && foundFeature && !( Object.keys(foundFeature?.getProperties()).length === 0 && foundFeature.constructor === Object ) ) { const layerName = getWrLayerNm(data); const isColumn = getFeatureCd( foundFeature.getProperties(), String(layerName) ); if (layerName && isColumn) featurePopUpOpen(layerName, isColumn, popupDetailRef); } }; useEffect(() => { if (isFeatured) { onFeatureSelect(isFeatured); } }, [isFeatured]); useEffect(() => { const bc = new BroadcastChannel("mapDetailPopUp"); bc.onmessage = (event) => { if (event.data.type === "SELECT_FEATURE") { onFeatureSelect(event.data.feature, true); closePopup(); } }; }, []); const renderDetailView = useCallback( (props: any) => { const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); return; } const isFeature = props?.featureData; if (typeof isFeature.getProperties === "function") { if ( !( Object.keys(isFeature?.getProperties()).length === 0 && isFeature.constructor === Object ) ) { const layerNm = getSelectedLayers(isFeature, gisMaps); const safeProps = isFeature.getProperties(); const featureCd = getFeatureCd(safeProps, String(layerNm)); if (layerNm) featurePopUpOpen(layerNm, featureCd, popupDetailRef); } } else { console.log(" data확인 필요"); } }, [checkedLayers, checkedLayersData, featureData] // 필요 시 의존성 넣기 ); const closePopup = () => { if (popupRef.current && !popupRef.current.closed) { popupRef.current.close(); popupRef.current = null; setIsOpen(false); setFeatureData(null); } }; const onOpenPopup = (data: any) => { if (data) { setIsOpen(true); setFeatureData(null); const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(data)}`; const width = window.screen.availWidth; const height = window.screen.availHeight; const left = 0; const top = 0; const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`; popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions); } else { toast.error("시설 및 수요처가 선택되지 않았습니다."); } }; const OVERLAY_MIN_ZOOM = 16; const OVERLAY_COORD_PRECISION = 5; const OVERLAY_MAX_PER_VIEW = 800; const overlayManagersRef = useRef([]); const overlayActiveRef = useRef(false); const moveEndDebounceRef = useRef(null); // 반환: Promise const overlayFormatter = async (feature: any, props: any) => { const p = props || {}; const featureId = feature.getId?.() ?? p?.id ?? p?.fid ?? p?.ftrIdn ?? ''; if (!featureId) return null; const tableId = String(featureId).split('.')[0]??''; const fcltCd = getFeatureCd(props, tableId); if (!fcltCd) return null; let data: any = null; let title = ''; if (tableId === 'tb_il_swage_prcs_fclt') { data = await pbnMapDetaile(fcltCd); title = '하수처리시설'; } else if (tableId === 'tb_wr_bdst_ruse_fclt') { data = await getRuseMaplListReturn(fcltCd); title = '하수재이용시설'; } else if (tableId === 'tb_wr_rnwtr_utztn_fclt'){ data = await rainMapDetile(fcltCd); title = '빗물이용시설'; } else if (tableId === 'tb_wr_rswtr_fclt') { data = await rswtMapDetile(fcltCd); title = '중수도시설'; } else if (tableId === 'tb_wr_htdfl_fclt') { data = await htdfMapDetile(fcltCd); title = '온배수시설'; } else if (tableId === 'tb_wr_ruse_fclt_dmd_co'){ data = await coSttnMapDetail(fcltCd); title = '수요처'; } else if (tableId === 'tb_wr_tmpr_dmd_co') { data = await ldrmntMapDetile(fcltCd); title = '잠재수요처'; } const empty = data == null || (Array.isArray(data) && data.length === 0) || (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0); if (empty) return null; const div = document.createElement('div'); const root = createRoot(div); (div as any).__dispose = () => { try { root.unmount(); } catch {} }; root.render(); return div; // ← 데이터 있을 때만 HTMLElement 반환 }; const destroyAllManagers = () => { overlayManagersRef.current.forEach(m => { try { m.destroy(); } catch {} }); overlayManagersRef.current = []; overlayActiveRef.current = false; }; const mountOverlayManagers = () => { const map = gisMaps.current; if (!map) return; // 이전 것 정리 destroyAllManagers(); // 체크된 벡터 레이어만 골라 매니저 생성 const checkedSet = new Set(checkedLayers.map(String)); const layers: any[] = map.getLayers().getArray(); const targets = layers.filter((layer: any) => { const src = layer.getSource?.(); const isVector = src && typeof src.getFeatures === "function"; const id = String(layer.get?.("lyrSn") ?? layer.get?.("id") ?? ""); return isVector && layer.getVisible?.() && checkedSet.has(id); }); targets.forEach((layer: any) => { const mgr = createOverlayManager(map, layer, overlayFormatter, { coordPrecision: OVERLAY_COORD_PRECISION, maxOverlaysPerViewport: OVERLAY_MAX_PER_VIEW, }); mgr.mount(); // 리스너 1회 등록 mgr.enable(); // 표시 + 1회 sync overlayManagersRef.current.push(mgr); }); overlayActiveRef.current = overlayManagersRef.current.length > 0; }; useEffect(() => { const map = gisMaps.current; if (!map) return; const z = map.getView()?.getZoom?.() ?? 0; if (z < OVERLAY_MIN_ZOOM) { // 저줌에선 그냥 안 보이게만 overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} }); return; } mountOverlayManagers(); return () => { destroyAllManagers(); }; }, [checkedLayers]); useEffect(() => { const map = gisMaps.current; if (!map) return; const onMoveStart = () => { // 숨기기만 (리스너/DOM 대량 제거 X) overlayManagersRef.current.forEach(m => { try { m.disable({ clear:false }); } catch {} }); if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; } }; const onMoveEnd = () => { const z = map.getView()?.getZoom?.() ?? 0; const active = overlayActiveRef.current; if (moveEndDebounceRef.current) clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = window.setTimeout(() => { if (z < OVERLAY_MIN_ZOOM) { if (active) overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} }); return; } if (!active) { mountOverlayManagers(); } else { // 다시 보이고 1회만 동기화 overlayManagersRef.current.forEach(m => { try { m.enable(); m.syncOverlays(); } catch {} }); } }, 180) as unknown as number; }; map.on("movestart", onMoveStart); map.on("moveend", onMoveEnd); return () => { map.un("movestart", onMoveStart); map.un("moveend", onMoveEnd); if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; } }; }, [checkedLayers]); return (
} waterDepth={undefined} onOpenPopup={(data: any) => { onOpenPopup(data); }} defaultZoom={7.5} defaultLonLat={[127.75, 36]} />
); }; export default Map;