435 lines
14 KiB
TypeScript
435 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import Gis from "@common/business_components/ui/map/Gis";
|
|
import { useEffect, useRef, useState, useCallback } from "react";
|
|
import { mapService } from "../../service";
|
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
|
import { SearchBar } from "@common/business_components/ui/map/searchBar/searchBar";
|
|
import { useWrStore } from "@/app/store";
|
|
import {
|
|
featurePopUpOpen,
|
|
getClickFeatureId,
|
|
getFeatureCd,
|
|
getSelectedLayers,
|
|
getWrFeaturePk,
|
|
getWrLayerNm,
|
|
} from "@/components/lib/func/mapUtil";
|
|
import toast, { Toaster } from "react-hot-toast";
|
|
import createOverlayManager from "@common/business_components/ui/map/GisOverlay";
|
|
import { createRoot } from "react-dom/client";
|
|
import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay";
|
|
|
|
const Map = () => {
|
|
|
|
const { code, comMap, rainMapDetile, rswtMapDetile, getRuseMaplListReturn, pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail } = useWrStore((state) => state);
|
|
const { isFeatured } = comMap;
|
|
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
|
const gisMaps = useRef<any | null>(null);
|
|
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
|
|
const [featureData, setFeatureData] = useState<any>(null); // 선택된 피쳐 데이터
|
|
const [checkedLayers, setCheckedLayers] = useState<string[]>([]);
|
|
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
|
|
const popupRef = useRef<Window | null>(null);
|
|
const popupDetailRef = useRef<Window | null>(null);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const response = await mapService.getLyrStngs("WR_NETWORK");
|
|
setTreeData(response); // API 응답 저장
|
|
} catch (error) {
|
|
console.error("데이터 불러오기 실패:", error);
|
|
}
|
|
};
|
|
|
|
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
|
|
// const handlePopupClose = useCallback(() => {
|
|
// setIsOpen(false);
|
|
// setFeatureData(null);
|
|
// }, []);
|
|
|
|
// 컴포넌트 마운트 시 1회 실행
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const messageHandler = (event: MessageEvent) => {
|
|
if (event.data.type === "POPUP_CLOSED") {
|
|
if (event.data.target === "detail") {
|
|
setFeatureData(null);
|
|
setIsOpen(false);
|
|
setIsSearchModalOpen(true);
|
|
popupDetailRef.current = null;
|
|
} else {
|
|
popupRef.current = null;
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener("message", messageHandler);
|
|
return () => {
|
|
window.removeEventListener("message", messageHandler);
|
|
};
|
|
}, []);
|
|
|
|
// 체크된 레이어 정보를 받는 핸들러
|
|
const handleCheckedLayersChange = (
|
|
checkedIds: string[],
|
|
checkedNodesData: any[]
|
|
) => {
|
|
setCheckedLayers(checkedIds);
|
|
setCheckedLayersData(checkedNodesData);
|
|
};
|
|
|
|
// SearchModal, GisContent 등에서 호출
|
|
const onFeatureFocus = (data: any) => {
|
|
console.log("data", data);
|
|
};
|
|
|
|
const onFeatureSelect = (data: any, popUpClick?: boolean) => {
|
|
const 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: any = null;
|
|
let featureId = data.fid;
|
|
|
|
if (popUpClick) {
|
|
const layerName = getWrLayerNm(data);
|
|
const featurePk = getWrFeaturePk(layerName ?? "");
|
|
|
|
// 재이용시설
|
|
if (featurePk) {
|
|
const lastPk =
|
|
layerName === "tb_wr_ruse_fclt_dmd_co"
|
|
? data[featurePk].replaceAll("-", "")
|
|
: data[featurePk];
|
|
featureId = `${layerName}.${lastPk}`;
|
|
|
|
} else {
|
|
console.warn("해당 피처는 선택된 레이어에서 찾을수 없습니다.");
|
|
}
|
|
} else {
|
|
featureId = getClickFeatureId(data);
|
|
}
|
|
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) {
|
|
const geometry = foundFeature.getGeometry();
|
|
if (geometry) {
|
|
const 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 {
|
|
// toast.error('위치정보를 찾을수 없습니다')
|
|
console.warn("OpenLayers Feature를 찾을 수 없습니다:");
|
|
console.log("foundFeature :", foundFeature);
|
|
console.warn("featureId : ", featureId);
|
|
console.warn("data : ", data);
|
|
console.warn(": 끝");
|
|
}
|
|
const currentId = data?.fid || data?.id || data?.ftrIdn;
|
|
const lastId = featureData?.fid || featureData?.id || featureData?.ftrIdn;
|
|
if (featureData && currentId === lastId) return; // 이전과 같으면 무시
|
|
setFeatureData(data);
|
|
setIsOpen(true);
|
|
setIsSearchModalOpen(false);
|
|
if (
|
|
popUpClick &&
|
|
foundFeature &&
|
|
!(
|
|
Object.keys(foundFeature?.getProperties()).length === 0 &&
|
|
foundFeature.constructor === Object
|
|
)
|
|
) {
|
|
const layerName = getWrLayerNm(data);
|
|
const isColumn = getFeatureCd(
|
|
foundFeature.getProperties(),
|
|
String(layerName)
|
|
);
|
|
if (layerName && isColumn)
|
|
featurePopUpOpen(layerName, isColumn, popupDetailRef);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isFeatured) {
|
|
|
|
onFeatureSelect(isFeatured);
|
|
}
|
|
}, [isFeatured]);
|
|
|
|
useEffect(() => {
|
|
const bc = new BroadcastChannel("mapDetailPopUp");
|
|
bc.onmessage = (event) => {
|
|
if (event.data.type === "SELECT_FEATURE") {
|
|
|
|
onFeatureSelect(event.data.feature, true);
|
|
closePopup();
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const renderDetailView = useCallback(
|
|
(props: any) => {
|
|
const map = gisMaps.current;
|
|
if (!map) {
|
|
console.warn("지도가 초기화되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
const isFeature = props?.featureData;
|
|
if (typeof isFeature.getProperties === "function") {
|
|
if (
|
|
!(
|
|
Object.keys(isFeature?.getProperties()).length === 0 &&
|
|
isFeature.constructor === Object
|
|
)
|
|
) {
|
|
const layerNm = getSelectedLayers(isFeature, gisMaps);
|
|
const safeProps = isFeature.getProperties();
|
|
const featureCd = getFeatureCd(safeProps, String(layerNm));
|
|
if (layerNm) featurePopUpOpen(layerNm, featureCd, popupDetailRef);
|
|
}
|
|
} else {
|
|
console.log(" data확인 필요");
|
|
}
|
|
},
|
|
[checkedLayers, checkedLayersData, featureData] // 필요 시 의존성 넣기
|
|
);
|
|
const closePopup = () => {
|
|
if (popupRef.current && !popupRef.current.closed) {
|
|
popupRef.current.close();
|
|
popupRef.current = null;
|
|
setIsOpen(false);
|
|
setFeatureData(null);
|
|
}
|
|
};
|
|
|
|
const onOpenPopup = (data: any) => {
|
|
if (data) {
|
|
setIsOpen(true);
|
|
setFeatureData(null);
|
|
const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(data)}`;
|
|
const width = window.screen.availWidth;
|
|
const height = window.screen.availHeight;
|
|
const left = 0;
|
|
const top = 0;
|
|
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
|
|
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
|
|
} else {
|
|
toast.error("시설 및 수요처가 선택되지 않았습니다.");
|
|
}
|
|
};
|
|
|
|
|
|
const OVERLAY_MIN_ZOOM = 16;
|
|
const OVERLAY_COORD_PRECISION = 5;
|
|
const OVERLAY_MAX_PER_VIEW = 800;
|
|
|
|
const overlayManagersRef = useRef<any[]>([]);
|
|
const overlayActiveRef = useRef(false);
|
|
const moveEndDebounceRef = useRef<number | null>(null);
|
|
|
|
// 반환: Promise<HTMLDivElement | null>
|
|
const overlayFormatter = async (feature: any, props: any) => {
|
|
const p = props || {};
|
|
const featureId =
|
|
feature.getId?.() ?? p?.id ?? p?.fid ?? p?.ftrIdn ?? '';
|
|
if (!featureId) return null;
|
|
|
|
|
|
const tableId = String(featureId).split('.')[0] ?? '';
|
|
const fcltCd = getFeatureCd(props, tableId);
|
|
|
|
if (!fcltCd) return null;
|
|
|
|
let data: any = null;
|
|
let title = '';
|
|
|
|
if (tableId === 'tb_il_swage_prcs_fclt') { data = await pbnMapDetaile(fcltCd); title = '하수처리시설'; }
|
|
else if (tableId === 'tb_wr_bdst_ruse_fclt') { data = await getRuseMaplListReturn(fcltCd); title = '하수재이용시설'; }
|
|
else if (tableId === 'tb_wr_rnwtr_utztn_fclt') { data = await rainMapDetile(fcltCd); title = '빗물이용시설'; }
|
|
else if (tableId === 'tb_wr_rswtr_fclt') { data = await rswtMapDetile(fcltCd); title = '중수도시설'; }
|
|
else if (tableId === 'tb_wr_htdfl_fclt') { data = await htdfMapDetile(fcltCd); title = '온배수시설'; }
|
|
else if (tableId === 'tb_wr_ruse_fclt_dmd_co') { data = await coSttnMapDetail(fcltCd); title = '수요처'; }
|
|
else if (tableId === 'tb_wr_tmpr_dmd_co') { data = await ldrmntMapDetile(fcltCd); title = '잠재수요처'; }
|
|
|
|
const empty =
|
|
data == null ||
|
|
(Array.isArray(data) && data.length === 0) ||
|
|
(typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0);
|
|
|
|
if (empty) return null;
|
|
|
|
const div = document.createElement('div');
|
|
const root = createRoot(div);
|
|
(div as any).__dispose = () => { try { root.unmount(); } catch { } };
|
|
root.render(<WrOverLay title={title} data={data} code={code} />);
|
|
return div; // ← 데이터 있을 때만 HTMLElement 반환
|
|
};
|
|
const destroyAllManagers = () => {
|
|
overlayManagersRef.current.forEach(m => { try { m.destroy(); } catch { } });
|
|
overlayManagersRef.current = [];
|
|
overlayActiveRef.current = false;
|
|
};
|
|
|
|
const mountOverlayManagers = () => {
|
|
const map = gisMaps.current;
|
|
if (!map) return;
|
|
|
|
// 이전 것 정리
|
|
destroyAllManagers();
|
|
|
|
// 체크된 벡터 레이어만 골라 매니저 생성
|
|
const checkedSet = new Set(checkedLayers.map(String));
|
|
const layers: any[] = map.getLayers().getArray();
|
|
const targets = layers.filter((layer: any) => {
|
|
const src = layer.getSource?.();
|
|
const isVector = src && typeof src.getFeatures === "function";
|
|
const id = String(layer.get?.("lyrSn") ?? layer.get?.("id") ?? "");
|
|
return isVector && layer.getVisible?.() && checkedSet.has(id);
|
|
});
|
|
|
|
targets.forEach((layer: any) => {
|
|
const mgr = createOverlayManager(map, layer, overlayFormatter, {
|
|
coordPrecision: OVERLAY_COORD_PRECISION,
|
|
maxOverlaysPerViewport: OVERLAY_MAX_PER_VIEW,
|
|
});
|
|
mgr.mount(); // 리스너 1회 등록
|
|
mgr.enable(); // 표시 + 1회 sync
|
|
overlayManagersRef.current.push(mgr);
|
|
});
|
|
|
|
overlayActiveRef.current = overlayManagersRef.current.length > 0;
|
|
};
|
|
|
|
useEffect(() => {
|
|
const map = gisMaps.current;
|
|
if (!map) return;
|
|
|
|
const z = map.getView()?.getZoom?.() ?? 0;
|
|
if (z < OVERLAY_MIN_ZOOM) {
|
|
// 저줌에선 그냥 안 보이게만
|
|
overlayManagersRef.current.forEach(m => { try { m.disable({ clear: true, delayed: true }); } catch { } });
|
|
return;
|
|
}
|
|
mountOverlayManagers();
|
|
|
|
return () => { destroyAllManagers(); };
|
|
}, [checkedLayers]);
|
|
|
|
|
|
useEffect(() => {
|
|
const map = gisMaps.current;
|
|
if (!map) return;
|
|
|
|
const onMoveStart = () => {
|
|
// 숨기기만 (리스너/DOM 대량 제거 X)
|
|
overlayManagersRef.current.forEach(m => { try { m.disable({ clear: false }); } catch { } });
|
|
if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; }
|
|
};
|
|
|
|
const onMoveEnd = () => {
|
|
const z = map.getView()?.getZoom?.() ?? 0;
|
|
const active = overlayActiveRef.current;
|
|
|
|
if (moveEndDebounceRef.current) clearTimeout(moveEndDebounceRef.current);
|
|
moveEndDebounceRef.current = window.setTimeout(() => {
|
|
if (z < OVERLAY_MIN_ZOOM) {
|
|
if (active) overlayManagersRef.current.forEach(m => { try { m.disable({ clear: true, delayed: true }); } catch { } });
|
|
return;
|
|
}
|
|
if (!active) {
|
|
mountOverlayManagers();
|
|
} else {
|
|
// 다시 보이고 1회만 동기화
|
|
overlayManagersRef.current.forEach(m => { try { m.enable(); m.syncOverlays(); } catch { } });
|
|
}
|
|
}, 180) as unknown as number;
|
|
};
|
|
|
|
map.on("movestart", onMoveStart);
|
|
map.on("moveend", onMoveEnd);
|
|
return () => {
|
|
map.un("movestart", onMoveStart);
|
|
map.un("moveend", onMoveEnd);
|
|
if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; }
|
|
};
|
|
}, [checkedLayers]);
|
|
return (
|
|
<div className="bg-container">
|
|
<Gis
|
|
showZoomLevel={true}
|
|
treeData={treeData}
|
|
isSearch={false}
|
|
gisMaps={gisMaps}
|
|
isMiniMap={false}
|
|
searchView={undefined}
|
|
detailView={renderDetailView}
|
|
onFeatureSelect={onFeatureSelect}
|
|
setIsOpen={setIsOpen}
|
|
setFeatureData={setFeatureData}
|
|
featureData={featureData}
|
|
setIsSearchModalOpen={setIsSearchModalOpen}
|
|
isOpen={isOpen}
|
|
isSearchModalOpen={isSearchModalOpen}
|
|
onCheckedLayersChange={handleCheckedLayersChange}
|
|
imgVisble={true}
|
|
searchBar={
|
|
<SearchBar
|
|
gisMaps={gisMaps}
|
|
facilityTypes={[
|
|
{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" },
|
|
{ text: "하수재이용시설", val: "tb_wr_bdst_ruse_fclt" },
|
|
{ text: "빗물이용시설", val: "tb_wr_rnwtr_utztn_fclt" },
|
|
{ text: "중수도시설", val: "tb_wr_rswtr_fclt" },
|
|
{ text: "온배수시설", val: "tb_wr_htdfl_fclt" },
|
|
{ text: "수요처", val: "tb_wr_ruse_fclt_dmd_co" },
|
|
{ text: "잠재수요처", val: "tb_wr_tmpr_dmd_co" },
|
|
]}
|
|
onFeatureFocus={onFeatureFocus}
|
|
/>
|
|
}
|
|
waterDepth={undefined}
|
|
onOpenPopup={(data: any) => {
|
|
onOpenPopup(data);
|
|
}}
|
|
defaultZoom={7.5}
|
|
defaultLonLat={[127.75, 36]}
|
|
/>
|
|
<Toaster position="top-center" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Map;
|