diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx new file mode 100644 index 000000000..7185c9660 --- /dev/null +++ b/apps/sn/app/map/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import Gis, { useGisContext } from "@common/business_components/ui/map/Gis"; +import { mapService } from "@service"; +import { useCallback, useEffect, useRef, useState } from "react"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import SearchModal from "./SearchModal"; +import FeatureClickEventPopup from "./featureClickEventPopup"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response.data); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + let 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 = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + 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) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let 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 { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + return ( +
+ +
+ ); +}; + +export default Map; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js new file mode 100644 index 000000000..4fd9a8eb4 --- /dev/null +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -0,0 +1,246 @@ +import './lib/css/gis.common.css'; +import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import OlMap from 'ol/Map'; +import View from 'ol/View'; +import { defaults as defaultControls, OverviewMap } from 'ol/control'; +import { defaults } from 'ol/interaction'; +import * as olProj from 'ol/proj'; +import proj4 from 'proj4'; +import { register } from 'ol/proj/proj4'; +import GisApp from './lib/gis.core'; +import GisContent from './GisContent'; +import GisTree from './GisTree'; +import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; +import GisToolBar from './GisToolBar'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; + +export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); +export const useGisContext = () => useContext(GisContext); + +function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { + const mapRef = useRef(); + const [mapObject, setMapObject] = useState({}); + const [tabVisible, setTabVisible] = useState(true); + // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 + // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); + const mapInstanceRef = useRef(null); + const checkedLayerIds = useRef([]); + let gisMap = null; + const closePopup = () => { + setIsOpen(false); // 팝업 닫기 + setFeatureData(null) + }; + + // SearchModal, GisContent 등에서 호출 + const handleFeatureSelect = useCallback((data) => { + gisMap = mapInstanceRef.current; + + if (!gisMap) { + console.warn('지도가 초기화되지 않았습니다.'); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + gisMap.getLayers().getArray().forEach(layer => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + gisMap.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn('OpenLayers Feature를 찾을 수 없습니다:', data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }, [mapInstanceRef]); + + useEffect(() => { + if (!mapRef.current) return; + if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + + // EPSG:5179 등록 + proj4.defs([ + [ + 'EPSG:5179', + '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + ], + ]); + register(proj4); + + //맵 생성 + const map = new OlMap({ + layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], + renderer: 'webgl', + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [126.6757, 37.5585], + GisApp.FROM_PROJ, + GisApp.TO_PROJ + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + + gisMaps.current = map + + //미니맵 + if(isMiniMap){ + const overviewMap = new OverviewMap({ + layers: [GisApp.Module.core.baseMiniMapLayers['base']], + view: new View({ + projection: GisApp.TO_PROJ, + minZoom: 9, + }), + collapsed: false, + collapseLabel: '\u00BB', + label: '\u00AB', + }); + map.addControl(overviewMap); + } + + gisCore.__proto__.map = map; + gisCore.scaleControl(); + gisCore.changeBaseLayer('base'); + setMapObject({ map: map }); + mapInstanceRef.current = map; // 생성 완료 플래그 설정 + }, []); + + useEffect(() => { + if (!treeData || treeData.length === 0) return; + + gisCore.deleteAllOverlay(); + gisCore.deleteAllLayer(); + + const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); + GisApp.layerCode = layerCode; + + //setTreeData(data); + const layerItems = getLayerItems(layerCode); + //setEditLayerItems(prev => [...layerItems]) + }, [treeData]); + + const getLayerItems = (layerCode) => { + const layerItems = Object.entries(layerCode) + .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') + .map(([key, obj]) => ({ + ...obj, + name: obj.lyrNm, + label: obj.lyrNm, + value: obj.lyrSn + '', + })) + .sort((a, b) => { + if (a.sortOrdr > b.sortOrdr) return 1; + if (a.sortOrdr < b.sortOrdr) return -1; + return 0; + }); + return layerItems; + }; + + if (mapObject.map) { + try { + gisMap = mapObject.map; + } catch (e) { + console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 + } + } + + return ( + <> + +
+
+ + + {/* */} + + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && featureData && !isSearchModalOpen && ( + + )} + + + + + +
+ + {isSearch && ( +
+ +
+ )} + + + ); +} + +export default React.memo(Gis); \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js new file mode 100644 index 000000000..136f394ad --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -0,0 +1,79 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import GisApp from './lib/gis.core'; +import { useGisContext } from './Gis'; +import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; +import GisToolBar from './GisToolBar'; +import './lib/css/gis.edit.css'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; + +export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { + const { gisMap, gisCore } = useGisContext(); + const [visibleComponents, setVisibleComponents] = useState({}); + const addressResultLayer = useRef(null); + const [playShow, setPlayShow] = useState({ show: false }); + + //셀렉트 버튼 이벤트 + // const clickFeature = useCallback(feature => { + // if (feature) { + // const layer = feature.getLayer(); + // setVisibleComponents(prevState => ({ + // ...prevState, + // })); + // } + // }, []); + + const selectRslt = useCallback(feature => { + GisApp.Module.layer.highLightFeature( + [feature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + clickFeature(feature); + }, []); + + useEffect(() => { + if (gisMap) { + setVisibleComponents(prevState => ({ + ...prevState, + })); + } + }, [gisMap]); + + if (!gisMap) return null; + + return ( + <> + {/**/} + {/* + + */} + {/* */} + + {/* 검색 모달 */} + {isSearch && ( +
+ +
+ )} + + ); +} \ No newline at end of file