425 lines
13 KiB
JavaScript
425 lines
13 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";
|
|
import LoadingOverlay from "@common/business_components/ui/loadingBar";
|
|
|
|
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,
|
|
searchBar: SearchBar,
|
|
waterDepth: WaterDepth,
|
|
isPlaying = false,
|
|
setIsPlaying = () => { },
|
|
targetLayerTypes = [],
|
|
orderedLayers = [],
|
|
checkInterval = 3000,
|
|
height: height = "h-[calc(100vh-5rem)]",
|
|
onOpenPopup = (data) => { },
|
|
droughtDates = { startYmd: '', endYmd: '' },
|
|
defaultZoom = 15,
|
|
defaultLonLat = [127.4875, 37.4916],
|
|
}) {
|
|
const mapRef = useRef();
|
|
const [mapObject, setMapObject] = useState({});
|
|
const [tabVisible, setTabVisible] = useState(true);
|
|
const [layerNm, setLayerNm] = useState("");
|
|
const [totalSteps, setTotalSteps] = useState(0);
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
// 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([]);
|
|
const [isLoading, setIsLoading] = useState(true) //로딩 상태
|
|
|
|
let gisMap = null;
|
|
const closePopup = () => {
|
|
setIsOpen(false); // 팝업 닫기
|
|
setFeatureData(null);
|
|
};
|
|
|
|
// 피처 클릭시
|
|
const handleFeatureSelect = useCallback(
|
|
(data) => {
|
|
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
|
|
},
|
|
[mapInstanceRef],
|
|
);
|
|
|
|
// 지도 생성 및 초기화
|
|
useEffect(() => {
|
|
if (!mapRef.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);
|
|
|
|
let map = mapInstanceRef.current;
|
|
|
|
// OpenLayers 지도 객체 생성
|
|
if (!map) {
|
|
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: defaultZoom ?? gisCore.defaults.zoom,
|
|
center: olProj.transform(
|
|
defaultLonLat,
|
|
GisApp.FROM_PROJ,
|
|
GisApp.TO_PROJ,
|
|
),
|
|
minZoom: gisCore.defaults.minZoom,
|
|
maxZoom: gisCore.defaults.maxZoom,
|
|
}),
|
|
});
|
|
} else {
|
|
// 재마운트 시 target 없으면 다시 지정
|
|
if (!map.getTarget()) {
|
|
map.setTarget(mapRef.current);
|
|
}
|
|
}
|
|
|
|
map.on('loadstart', function () {
|
|
setIsLoading(false)
|
|
});
|
|
|
|
const select = GisApp.Module.select;
|
|
if (select) {
|
|
if (select.selectInteraction) {
|
|
map.removeInteraction(select.selectInteraction);
|
|
}
|
|
select.map = map; // map 바인딩
|
|
select.setInteraction();
|
|
select.pointerMoveEventHandler();
|
|
}
|
|
|
|
// 미니맵 추가
|
|
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);
|
|
}
|
|
|
|
gisMaps.current = map;
|
|
|
|
// 스케일 컨트롤 및 베이스 레이어 설정
|
|
gisCore.__proto__.map = map;
|
|
gisCore.scaleControl();
|
|
gisCore.changeBaseLayer("base");
|
|
setMapObject({ map: map });
|
|
mapInstanceRef.current = map; // 생성 완료 플래그 설정
|
|
|
|
// 📌 언마운트 시 완전 제거
|
|
return () => {
|
|
map.setTarget(null); // DOM 연결 해제
|
|
mapInstanceRef.current = null; // 참조 초기화
|
|
};
|
|
}, []);
|
|
|
|
// treeData 변경 시 레이어 코드 생성
|
|
useEffect(() => {
|
|
if (!treeData || treeData.length === 0) return;
|
|
|
|
// 기존 오버레이 및 레이어 모두 삭제
|
|
gisCore.deleteAllOverlay();
|
|
gisCore.deleteAllLayer();
|
|
|
|
// 트리 데이터를 레이어 코드 객체로 변환
|
|
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn");
|
|
GisApp.layerCode = layerCode; // 전역 레이어 코드 설정
|
|
}, [treeData]);
|
|
|
|
// droughtDates를 전역(GisApp)에 반영
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
if (droughtDates?.startYmd && droughtDates?.endYmd) {
|
|
GisApp.DROUGHT_DATES = {
|
|
startYmd: String(droughtDates.startYmd),
|
|
endYmd: String(droughtDates.endYmd),
|
|
};
|
|
}
|
|
}, [droughtDates?.startYmd, droughtDates?.endYmd]);
|
|
|
|
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); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
|
|
}
|
|
}
|
|
|
|
// 체크된 레이어 정보를 처리하는 함수
|
|
const handleCheckedLayersChange = useCallback(
|
|
(checkedIds, checkedNodesData) => {
|
|
setTimeout(() => {
|
|
setCheckedLayers(checkedIds);
|
|
setCheckedLayersData(checkedNodesData);
|
|
onCheckedLayersChange?.(checkedIds, checkedNodesData);
|
|
}, 1000);
|
|
},
|
|
[onCheckedLayersChange],
|
|
);
|
|
|
|
// 진행률 변경 핸들러
|
|
const handleProgressChange = useCallback((total, current) => {
|
|
setTotalSteps(total);
|
|
setCurrentStep(current);
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<LoadingOverlay isLoading={isLoading ?? false} />
|
|
<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={(layerNm) => {
|
|
layerNmRef.current = layerNm;
|
|
setLayerNm(layerNm);
|
|
setIsOpen(true);
|
|
onOpenPopup(layerNm)
|
|
}}
|
|
imageVisible={imgVisble}
|
|
isPlaying={isPlaying}
|
|
setIsPlaying={setIsPlaying}
|
|
targetLayerTypes={targetLayerTypes}
|
|
orderedLayers={orderedLayers}
|
|
checkInterval={checkInterval}
|
|
onProgressChange={handleProgressChange}
|
|
/>
|
|
|
|
{typeof WaterDepth === "function" && (
|
|
<div className={`absolute bottom-4 z-40 ${tabVisible ? 'left-[260px]' : 'left-4'}`}>
|
|
<WaterDepth
|
|
onClose={() => setIsOpen(false)}
|
|
isPlaying={isPlaying}
|
|
setIsPlaying={setIsPlaying}
|
|
totalSteps={totalSteps}
|
|
currentStep={currentStep}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
|
|
{isOpen && !isSearchModalOpen && DetailView && (
|
|
<DetailView
|
|
featureData={featureData || { getProperties: () => ({}) }}
|
|
seletectedLayerNm={layerNm}
|
|
onClose={closePopup} // 닫기 콜백 전달
|
|
onFeatureSelect={handleFeatureSelect}
|
|
/>
|
|
)}
|
|
{/* SearchView가 존재할 때만 렌더링 */}
|
|
{SearchView && (
|
|
<SearchView
|
|
open={isSearchModalOpen}
|
|
onOpenChange={setIsSearchModalOpen}
|
|
onFeatureSelect={handleFeatureSelect}
|
|
/>
|
|
)}
|
|
{/* 시도/시군구 조회 영역 */}
|
|
{SearchBar && (
|
|
<div className="absolute left-1/2 top-4 z-30 w-max -translate-x-1/2 rounded">
|
|
{SearchBar}
|
|
</div>
|
|
)}
|
|
<GisToolBar />
|
|
<GisMapToggle />
|
|
<GisSelect onFeatureSelect={handleFeatureSelect} />
|
|
</div>
|
|
</div>
|
|
|
|
{isSearch && (
|
|
<div className="absolute right-8 top-[305px] z-20">
|
|
<button
|
|
onClick={() => setIsSearchModalOpen(true)}
|
|
title="정보 검색"
|
|
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>
|
|
</div>
|
|
)}
|
|
</GisContext.Provider>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default React.memo(Gis);
|