feat: Gis.js에 침수심 범례 추가

This commit is contained in:
swcho
2025-08-05 10:55:47 +09:00
parent b2a6285475
commit ab63cbe992
@@ -1,28 +1,36 @@
import { Search } from 'lucide-react';
import OlMap from 'ol/Map';
import Overlay from 'ol/Overlay';
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 { register } from 'ol/proj/proj4';
import proj4 from 'proj4';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import GisLonLat from './GisLonLat';
import GisMapToggle from './GisMapToggle';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import GisTree from './GisTree';
import './lib/css/gis.common.css';
import GisApp from './lib/gis.core';
// import FacilityInfoPopup from './FacilityInfoPopup';
import { Search } from "lucide-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 { register } from "ol/proj/proj4";
import proj4 from "proj4";
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import GisMapToggle from "./GisMapToggle";
import GisSelect from "./GisSelect";
import GisToolBar from "./GisToolBar";
import GisTree from "./GisTree";
import "./lib/css/gis.common.css";
import GisApp from "./lib/gis.core";
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
export const GisContext = createContext({
gisCore: null,
gisMap: null,
treeData: null,
checkedLayerIds: null,
});
export const useGisContext = () => useContext(GisContext);
export const searchUpperPpls = async (fcltMngNo) => {
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || '';
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
@@ -31,31 +39,35 @@ export const searchUpperPpls = async (fcltMngNo) => {
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
const contentLength = res.headers.get("content-length");
if (contentLength === "0")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
if (!text || text.trim() === "")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// JSON 파싱 시도
data = JSON.parse(text);
} catch (error) {
console.error('JSON parsing error:', error);
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
}
}
const select = GisApp.Module.select;
const gisMap = GisApp.Module.core?.map;
let foundFeature = null;
data.forEach(item => {
data.forEach((item) => {
const pplMngNo = item.ppl_mng_no.trim();
gisMap.getLayers().getArray().filter(e => e.get("lyrTyNm") == 'tb_ms_swage_ppl').forEach(layer => {
gisMap
.getLayers()
.getArray()
.filter((e) => e.get("lyrTyNm") == "tb_ms_swage_ppl")
.forEach((layer) => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
@@ -87,9 +99,8 @@ export const searchUpperPpls = async (fcltMngNo) => {
}
};
function Gis({
isOpen,
isOpen, //우측 상세 레이어 팝업
gisMaps,
isSearchModalOpen,
treeData = [],
@@ -103,19 +114,25 @@ function Gis({
detailView: DetailView,
onFeatureSelect,
setIsSearchModalOpen,
onCheckedLayersChange
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth,
height: height = "h-[calc(100vh-10rem)]",
}) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [layerNm, setLayerNm] = useState('')
const [layerNm, setLayerNm] = useState("");
// 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 gisCore = useMemo(
() => new GisApp.core({ targetId: mapRef.current }),
[],
);
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
const layerNmRef = useRef('')
const layerNmRef = useRef("");
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
@@ -127,97 +144,16 @@ function Gis({
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
setFeatureData(null);
};
// Feature Click Event
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 인스턴스를 찾아서 사용
let featureId = data.fid;
const layerName = layerNmRef.current
if (layerName === '하수재이용시설') {
featureId = data.searchPk;
} else if (layerName === '빗물이용시설' || layerName == '중수도시설') {
featureId = data.bdstInfoSn;
} else if (layerName === '하수처리시설') {
featureId = data.bscFctCd;
} else if (layerName === '온배수시설') {
featureId = data.bdstRuseFcltSn;
} else if(layerName == '수요처'){
featureId = data.dmdInfoId.replaceAll('-','')
}
else {
console.warn('해당 레이어에 대한 featureId 매핑 없음', layerNm);
featureId = data.fid || data.id || data.ftrIdn;
}
// gisMap.getLayers().getArray().forEach(layer => {
// const source = layer.getSource?.();
// if (source?.getFeatureById) {
// const feature = source.getFeatureById(String(featureId).trim());
// if (feature) foundFeature = feature;
// }
// });
// const [layerType, id] = String(featureId).split('.');
gisMap.getLayers().getArray().forEach(layer => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const allFeatures = source.getFeatures();
for (const f of allFeatures) {
const id = String(f.getId());
// console.log('id', id, featureId)
if (id && id.split('.')[1] === String(featureId)) {
foundFeature = f;
break;
}
}
}
});
}
// 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);
const handleFeatureSelect = useCallback(
(data) => {
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
}, [mapInstanceRef]);
},
[mapInstanceRef],
);
// 지도 생성 및 초기화
useEffect(() => {
@@ -227,16 +163,16 @@ function Gis({
// 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',
"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);
// OpenLayers 지도 객체 생성
const map = new OlMap({
layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']],
renderer: 'webgl',
layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]],
renderer: "webgl",
target: mapRef.current,
interactions: defaults({ doubleClickZoom: false }),
controls: defaultControls({
@@ -252,26 +188,26 @@ function Gis({
center: olProj.transform(
[127.4875, 37.4916],
GisApp.FROM_PROJ,
GisApp.TO_PROJ
GisApp.TO_PROJ,
),
minZoom: gisCore.defaults.minZoom,
maxZoom: gisCore.defaults.maxZoom,
}),
});
gisMaps.current = map
gisMaps.current = map;
// 미니맵 추가
if (isMiniMap) {
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
layers: [GisApp.Module.core.baseMiniMapLayers["base"]],
view: new View({
projection: GisApp.TO_PROJ,
minZoom: 9,
}),
collapsed: false,
collapseLabel: '\u00BB',
label: '\u00AB',
collapseLabel: "\u00BB",
label: "\u00AB",
});
map.addControl(overviewMap);
}
@@ -279,7 +215,7 @@ function Gis({
// 스케일 컨트롤 및 베이스 레이어 설정
gisCore.__proto__.map = map;
gisCore.scaleControl();
gisCore.changeBaseLayer('base');
gisCore.changeBaseLayer("base");
setMapObject({ map: map });
mapInstanceRef.current = map; // 생성 완료 플래그 설정
}, []);
@@ -293,23 +229,23 @@ function Gis({
gisCore.deleteAllLayer();
// 트리 데이터를 레이어 코드 객체로 변환
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn");
GisApp.layerCode = layerCode; // 전역 레이어 코드 설정
//setTreeData(data);
// 편집 가능한 레이어 목록 생성
const layerItems = getLayerItems(layerCode);
//const layerItems = getLayerItems(layerCode);
//setEditLayerItems(prev => [...layerItems])
}, [treeData]);
const getLayerItems = (layerCode) => {
const layerItems = Object.entries(layerCode)
.filter(([key, obj], idx) => obj?.updtPsblYn === 'Y')
.filter(([key, obj], idx) => obj?.updtPsblYn === "Y")
.map(([key, obj]) => ({
...obj,
name: obj.lyrNm,
label: obj.lyrNm,
value: obj.lyrSn + '',
value: obj.lyrSn + "",
}))
.sort((a, b) => {
if (a.sortOrdr > b.sortOrdr) return 1;
@@ -323,145 +259,47 @@ function Gis({
try {
gisMap = mapObject.map;
} catch (e) {
console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
console.error("오류 발생:", e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
// // 데이터 로드 및 팝업 생성 로직
// useEffect(() => {
// if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함
// // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사)
// const fetchPopupData = async () => {
// // 실제 API 호출 로직으로 대체해야 합니다.
// // 예시: const response = await yourApiService.getFcltyWtCnt();
// // setFacilityInfoPopupData(response.data);
// // 임시 목업 데이터
// const mockData = [
// { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 },
// { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 }
// ];
// setFacilityInfoPopupData(mockData);
// };
// fetchPopupData();
// }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행
// // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직
// useEffect(() => {
// const map = mapInstanceRef.current;
// if (!map || facilityInfoPopupData.length === 0) return;
// // 기존 오버레이 정리
// facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay));
// facilityOverlaysRef.current = [];
// facilityInfoPopupData.forEach(data => {
// const popupId = `${data.ftrIdn}_${data.ftrCde}`;
// // 팝업을 담을 div 엘리먼트 생성
// const popupElement = document.createElement('div');
// popupElement.id = popupId;
// // OpenLayers Overlay 생성
// const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
// const overlay = new Overlay({
// element: popupElement,
// position: position,
// offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정
// positioning: 'bottom-center',
// className: 'facility-popup-overlay' // 필요시 공통 클래스 추가
// });
// map.addOverlay(overlay);
// facilityOverlaysRef.current.push(overlay);
// // React 컴포넌트를 popupElement에 렌더링
// const root = createRoot(popupElement);
// root.render(<FacilityInfoPopup data={data} />);
// });
// // 초기 가시성 업데이트
// updatePopupVisibility();
// }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행
// // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수
// const updatePopupVisibility = useCallback(() => {
// const map = mapInstanceRef.current;
// if (!map) return;
// const visibleLayers = new Set();
// map.getLayers().forEach(layer => {
// if (layer.getVisible()) {
// // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링
// const typeName = layer.get('typeName');
// if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름
// visibleLayers.add(layer.get('ftrCde'));
// }
// }
// });
// facilityOverlaysRef.current.forEach(overlay => {
// const element = overlay.getElement();
// const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기
// if (visibleLayers.has(ftrCde)) {
// element.style.display = '';
// } else {
// element.style.display = 'none';
// }
// });
// }, []);
// GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다.
// 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출
// useEffect(() => {
// // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트
// const map = mapInstanceRef.current;
// if (!map) return;
// const handleLayerVisibilityChange = () => {
// updatePopupVisibility();
// };
// map.getLayers().on('change:length', handleLayerVisibilityChange);
// map.getLayers().forEach(layer => {
// layer.on('change:visible', handleLayerVisibilityChange);
// });
// return () => {
// // 클린업
// map.getLayers().un('change:length', handleLayerVisibilityChange);
// map.getLayers().forEach(layer => {
// layer.un('change:visible', handleLayerVisibilityChange);
// });
// };
// }, [updatePopupVisibility]);
// 체크된 레이어 정보를 처리하는 함수
const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => {
const handleCheckedLayersChange = useCallback(
(checkedIds, checkedNodesData) => {
setTimeout(() => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, 0);
}, [onCheckedLayersChange]);
}, 1000);
},
[onCheckedLayersChange],
);
return (
<>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisContext.Provider
value={{
gisCore,
gisMap: mapInstanceRef.current,
treeData,
checkedLayerIds,
}}
>
<div className={`relative flex ${height}`}>
<div
ref={mapRef}
className="gis-map relative flex-1"
style={{ height: "100%", padding: 0 }}
>
<GisTree
visible={tabVisible}
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(nodeNm) => {
layerNmRef.current = nodeNm
setLayerNm(nodeNm); setIsOpen(true); const dummyFeature = {
onLayerIconClick={(layerNm) => {
layerNmRef.current = layerNm;
setLayerNm(layerNm);
setIsOpen(true);
const dummyFeature = {
getProperties: () => ({}), // 최소 구조
};
onFeatureSelect(dummyFeature);
@@ -469,7 +307,13 @@ function Gis({
imageVisible={imgVisble}
/>
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
{typeof WaterDepth === "function" && (
<div className={`absolute bottom-4 z-40 ${tabVisible ? 'left-[260px]' : 'left-4'}`}>
<WaterDepth
onClose={() => setIsOpen(false)}
/>
</div>
)}
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && !isSearchModalOpen && (
@@ -488,23 +332,25 @@ function Gis({
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* <GisLonLat /> */}
{/* 시도/시군구 조회 영역 */}
{SearchBar && (
<div
className="absolute left-1/2 top-4 z-30 -translate-x-1/2 rounded w-max"
>
<SearchBar />
</div>
)}
<GisToolBar />
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
</div>
</div>
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<button
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
className="flex h-[40px] w-[40px] items-center justify-center overflow-hidden rounded border border-[#9aa7b8] bg-white p-0 shadow-[0_0_5px_#999]"
>
<Search size={24} className="text-gray-700" />
</button>