526 lines
17 KiB
JavaScript
526 lines
17 KiB
JavaScript
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 useGisContext = () => useContext(GisContext);
|
|
export const searchUpperPpls = async (fcltMngNo) => {
|
|
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || '';
|
|
|
|
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
|
method: "GET",
|
|
});
|
|
|
|
let data = null;
|
|
if (res.ok) {
|
|
try {
|
|
// 응답이 비어있는지 확인
|
|
const contentLength = res.headers.get('content-length');
|
|
if (contentLength === '0') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
|
|
|
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
|
const text = await res.text();
|
|
if (!text || text.trim() === '') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
|
|
|
// JSON 파싱 시도
|
|
data = JSON.parse(text);
|
|
} catch (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 => {
|
|
const pplMngNo = item.ppl_mng_no.trim();
|
|
|
|
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();
|
|
for (const f of features) {
|
|
if (f.values_.fclt_mng_no.trim() === pplMngNo) {
|
|
foundFeature = f;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (foundFeature) select.selectMultiFeature(foundFeature, true);
|
|
});
|
|
|
|
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
|
|
|
let geometry = foundFeature?.getGeometry();
|
|
if (geometry) {
|
|
let extent = geometry.getExtent();
|
|
|
|
gisMap.getView().fit(extent, {
|
|
duration: 300,
|
|
padding: [50, 50, 50, 50],
|
|
maxZoom: 14,
|
|
constrainResolution: true,
|
|
nearest: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
function Gis({
|
|
isOpen, //우측 상세 레이어 팝업
|
|
gisMaps,
|
|
isSearchModalOpen,
|
|
treeData = [],
|
|
isSearch = false,
|
|
isMiniMap = false,
|
|
setIsOpen,
|
|
setFeatureData,
|
|
featureData,
|
|
imgVisble = false,
|
|
searchView: SearchView,
|
|
detailView: DetailView,
|
|
onFeatureSelect,
|
|
setIsSearchModalOpen,
|
|
onCheckedLayersChange,
|
|
}) {
|
|
const mapRef = useRef();
|
|
const [mapObject, setMapObject] = useState({});
|
|
const [tabVisible, setTabVisible] = useState(true);
|
|
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 mapInstanceRef = useRef(null);
|
|
const checkedLayerIds = useRef([]);
|
|
const layerNmRef = useRef('')
|
|
|
|
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
|
|
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
|
|
// const facilityOverlaysRef = useRef([]);
|
|
// 체크된 레이어 상태 관리
|
|
const [checkedLayers, setCheckedLayers] = useState([]);
|
|
const [checkedLayersData, setCheckedLayersData] = useState([]);
|
|
|
|
let gisMap = null;
|
|
const closePopup = () => {
|
|
setIsOpen(false); // 팝업 닫기
|
|
setFeatureData(null)
|
|
};
|
|
|
|
// Feature Click Event
|
|
const handleFeatureSelect = useCallback((data) => {
|
|
console.log(data);
|
|
// gisMap = mapInstanceRef.current;
|
|
// gisMap = mapInstanceRef.current;
|
|
|
|
// if (!gisMap) {
|
|
// console.warn('지도가 초기화되지 않았습니다.');
|
|
// return;
|
|
// }
|
|
// if (!gisMap) {
|
|
// console.warn('지도가 초기화되지 않았습니다.');
|
|
// return;
|
|
// }
|
|
|
|
// // 항상 같은 select 객체 사용
|
|
// const select = GisApp.Module.select;
|
|
// if (!select) 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);
|
|
|
|
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
|
|
console.log(data)
|
|
}, [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);
|
|
|
|
// OpenLayers 지도 객체 생성
|
|
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(
|
|
[127.4875, 37.4916],
|
|
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; // 생성 완료 플래그 설정
|
|
|
|
}, []);
|
|
|
|
// treeData 변경 시 레이어 코드 생성
|
|
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); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
|
|
}
|
|
}
|
|
|
|
// // 데이터 로드 및 팝업 생성 로직
|
|
// 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) => {
|
|
setTimeout(() => {
|
|
setCheckedLayers(checkedIds);
|
|
setCheckedLayersData(checkedNodesData);
|
|
onCheckedLayersChange?.(checkedIds, checkedNodesData);
|
|
}, 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 }} />
|
|
<GisTree
|
|
visible={tabVisible}
|
|
setVisible={setTabVisible}
|
|
onCheckedChange={handleCheckedLayersChange}
|
|
onLayerIconClick={(layerNm) => {
|
|
layerNmRef.current = layerNm
|
|
setLayerNm(layerNm); setIsOpen(true); const dummyFeature = {
|
|
getProperties: () => ({}), // 최소 구조
|
|
};
|
|
onFeatureSelect(dummyFeature);
|
|
}}
|
|
imageVisible={imgVisble}
|
|
/>
|
|
|
|
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
|
|
|
|
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
|
|
{isOpen && !isSearchModalOpen && (
|
|
<DetailView
|
|
featureData={featureData || { getProperties: () => ({}) }}
|
|
seletectedLayerNm={layerNm}
|
|
onClose={closePopup} // 닫기 콜백 전달
|
|
onFeatureSelect={handleFeatureSelect}
|
|
/>
|
|
)}
|
|
{/* SearchView가 존재할 때만 렌더링 */}
|
|
{SearchView && (
|
|
<SearchView
|
|
open={isSearchModalOpen}
|
|
onOpenChange={setIsSearchModalOpen}
|
|
onFeatureSelect={handleFeatureSelect}
|
|
/>
|
|
)}
|
|
{/* <GisLonLat /> */}
|
|
<GisToolBar />
|
|
<GisMapToggle />
|
|
<GisSelect onFeatureSelect={handleFeatureSelect} />
|
|
</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
|
|
"
|
|
>
|
|
<Search size={24} className="text-gray-700" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</GisContext.Provider>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default React.memo(Gis);
|