Files
frontend/packages/common/business_components/src/ui/map/Gis.js
T
2025-08-14 15:05:05 +09:00

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);