324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import Gis from "@common/business_components/ui/map/Gis";
|
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { SearchBar } from "@common/business_components/ui/map/searchBar";
|
|
|
|
// OpenLayers overlay/extent 유틸
|
|
import Overlay from "ol/Overlay";
|
|
import { getCenter as getExtentCenter } from "ol/extent";
|
|
import type { Coordinate } from "ol/coordinate";
|
|
|
|
interface FeatureData {
|
|
fid?: string;
|
|
ftrIdn?: string;
|
|
id?: string;
|
|
getId?: () => string | number | undefined; // 추가
|
|
// OpenLayers Feature 비슷한 최소 인터페이스
|
|
get?: (key: string) => any;
|
|
getGeometry?: () => {
|
|
getExtent: () => number[];
|
|
getType?: () => string;
|
|
getCoordinates?: () => Coordinate;
|
|
};
|
|
setStyle?: () => void;
|
|
}
|
|
|
|
interface MapInstance {
|
|
addOverlay?: (ov: Overlay) => void;
|
|
removeOverlay?: (ov: Overlay) => void;
|
|
getOverlays?: () => { getArray: () => Overlay[] };
|
|
getLayers: () => {
|
|
getArray: () => Array<{
|
|
get: (key: string) => any;
|
|
getSource?: () => { getFeatureById?: (id: string) => FeatureData | null };
|
|
}>;
|
|
};
|
|
getView: () => { fit: (extent: number[], options: object) => void };
|
|
}
|
|
|
|
const Map = () => {
|
|
const [treeData, setTreeData] = useState([]);
|
|
const gisMaps = useRef<MapInstance | null>(null);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
|
const [featureData, setFeatureData] = useState<FeatureData | null>(null);
|
|
|
|
// === Overlay 관리용 Ref ===
|
|
const overlayRef = useRef<Overlay | null>(null);
|
|
const overlayContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const facilityTypes = [
|
|
{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" },
|
|
];
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const API_URL = "/sewerage-service/api/v1/map";
|
|
const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`;
|
|
const response = await fetch(reqUrl, {
|
|
method: "GET",
|
|
headers: { accept: "application/json" },
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP 오류! 상태: ${response.status}`);
|
|
const data = await response.json();
|
|
setTreeData(data);
|
|
} catch (error) {
|
|
console.error("데이터 불러오기 실패:", error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
function formatWithCommas(value: any) {
|
|
if (value === null || value === undefined) return value;
|
|
const num = Number(value);
|
|
return isNaN(num) ? value : num.toLocaleString();
|
|
}
|
|
|
|
function formatDateString(dateStr: string) {
|
|
dateStr = dateStr.trim();
|
|
if (typeof dateStr !== "string" || dateStr.length !== 8) return dateStr;
|
|
return `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6)}`;
|
|
}
|
|
|
|
// ===== 오버레이 콘텐츠 빌더 =====
|
|
const buildOverlayContent = (layerType: string, feature: FeatureData) => {
|
|
const props: Record<string, any> = {};
|
|
// 안전하게 몇 개 필드 추출(없으면 건너뜀)
|
|
const mayGet = (k: string) => (feature.get ? feature.get(k) : undefined);
|
|
const safe = (v: any) => (v === undefined || v === null ? "-" : String(v));
|
|
|
|
// 레이어 유형별 대표 필드 추출(없으면 "-")
|
|
if (layerType === "tb_il_swage_prcs_fclt") {
|
|
// 하수처리시설
|
|
props["하수처리시설명"] = mayGet("swtrmt_fclt_nm");
|
|
props["시설용량(m³/d)"] = formatWithCommas(mayGet("fclt_cpct"));
|
|
props["주소"] = mayGet("rdnm_addr");
|
|
props["가동개시일"] = formatDateString(mayGet("oprtng_strt_ymd"));
|
|
props["공법"] = mayGet("rprs_msrc_cn");
|
|
} else if (layerType === "tb_ms_swage_prcs_zone") {
|
|
// 처리구역(존)
|
|
props["하수처리구역명"] = mayGet("swtrmt_zone_nm");
|
|
props["하수처리시설명"] = mayGet("swtrmt_fclt_nm");
|
|
} else if (layerType === "om_wqlt_station") {
|
|
// 수질관측소
|
|
props["관측소명"] = mayGet("stn_ko") ?? mayGet("name");
|
|
props["주소"] = mayGet("addr") ?? mayGet("indicator");
|
|
}
|
|
|
|
const titleMap: Record<string, string> = {
|
|
tb_il_swage_prcs_fclt: "하수처리시설",
|
|
tb_ms_swage_prcs_zone: "하수처리구역",
|
|
om_wqlt_station: "관측소",
|
|
};
|
|
const title = titleMap[layerType] || "정보";
|
|
|
|
// 컨테이너 재사용(없으면 생성)
|
|
if (!overlayContainerRef.current) {
|
|
overlayContainerRef.current = document.createElement("div");
|
|
}
|
|
const el = overlayContainerRef.current;
|
|
el.className =
|
|
"ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] ";
|
|
el.innerHTML = `
|
|
<div class="font-semibold text-lg text-gray-800 mb-2 text-center bg-green-400">${title}</div>
|
|
<table class="w-full text-base text-gray-800">
|
|
${Object.entries(props)
|
|
.map(
|
|
([k, v]) => `
|
|
<tr>
|
|
<td class="py-1 pr-2 text-gray-500 whitespace-nowrap">${k}</td>
|
|
<td class="py-1 font-medium">${safe(v)}</td>
|
|
</tr>`
|
|
)
|
|
.join("")}
|
|
</table>
|
|
`;
|
|
return el;
|
|
};
|
|
|
|
// ===== 오버레이 생성/표시 =====
|
|
const showOverlay = (
|
|
map: MapInstance,
|
|
feature: FeatureData,
|
|
layerType: string
|
|
) => {
|
|
// 이전 오버레이 제거
|
|
if (overlayRef.current) {
|
|
map.removeOverlay?.(overlayRef.current);
|
|
overlayRef.current = null;
|
|
}
|
|
|
|
// 콘텐츠 만들기
|
|
const element = buildOverlayContent(layerType, feature);
|
|
|
|
// 앵커 좌표 계산(포인트면 좌표, 아니면 extent 중심)
|
|
let position: Coordinate | undefined;
|
|
const geom = feature.getGeometry?.();
|
|
if (geom?.getType && geom.getType() === "Point" && geom.getCoordinates) {
|
|
position = geom.getCoordinates();
|
|
} else if (geom?.getExtent) {
|
|
position = getExtentCenter(geom.getExtent());
|
|
}
|
|
|
|
if (!position) return;
|
|
|
|
// 오버레이 생성 & 지도에 추가
|
|
const overlay = new Overlay({
|
|
element,
|
|
positioning: "bottom-center",
|
|
stopEvent: false,
|
|
offset: [0, -12],
|
|
});
|
|
overlay.setPosition(position);
|
|
map.addOverlay?.(overlay);
|
|
overlayRef.current = overlay;
|
|
};
|
|
|
|
// ===== 피처 선택 시 호출 =====
|
|
const onFeatureSelect = (data: FeatureData) => {
|
|
const map = gisMaps.current;
|
|
if (!map) {
|
|
console.warn("지도가 초기화되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
type SelectModule = {
|
|
resetSelectedFeature: () => void;
|
|
selectFeature: (feature: FeatureData, selected: boolean) => void;
|
|
};
|
|
|
|
const maybeSelect = GisApp?.Module?.select; // 타입 그대로 둠 (any | unknown | null 가능)
|
|
|
|
if (!maybeSelect) {
|
|
// null/undefined면 일찍 반환
|
|
return;
|
|
}
|
|
|
|
const select = maybeSelect as SelectModule; // 여기선 null 아님이 보장됨
|
|
if (!select) return;
|
|
|
|
// 1) 기존 선택 초기화
|
|
select.resetSelectedFeature();
|
|
|
|
// 2) 피처 탐색 + 레이어 유형 파악
|
|
const featureId = String(data.getId?.() ?? "");
|
|
const foundLayerType = featureId.split(".")[0];
|
|
let foundFeature: FeatureData | null = null;
|
|
const isOlFeatureInstance =
|
|
data &&
|
|
typeof data.getGeometry === "function" &&
|
|
typeof data.setStyle === "function";
|
|
|
|
if (isOlFeatureInstance) {
|
|
foundFeature = data;
|
|
// 어떤 레이어에서 온 건지 모르는 경우가 많아 기본값으로 시도
|
|
} else {
|
|
map
|
|
.getLayers()
|
|
.getArray()
|
|
.forEach((layer) => {
|
|
if (foundFeature) return; // 이미 찾았으면 skip
|
|
const source = layer.getSource?.();
|
|
const feature = source?.getFeatureById?.(featureId);
|
|
if (feature) {
|
|
foundFeature = feature;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 3) 피처 선택/줌 + 오버레이
|
|
if (foundFeature && foundFeature.getGeometry) {
|
|
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);
|
|
|
|
// 표출 대상 레이어만 오버레이 표시
|
|
const normalized = (foundLayerType || "").toString().trim();
|
|
const TARGETS = [
|
|
"tb_il_swage_prcs_fclt",
|
|
"tb_ms_swage_prcs_zone",
|
|
"om_wqlt_station",
|
|
];
|
|
|
|
if (TARGETS.includes(normalized)) {
|
|
showOverlay(map, foundFeature, normalized);
|
|
} else {
|
|
// 대상 외 레이어 클릭 시 기존 오버레이 제거
|
|
if (overlayRef.current) {
|
|
map.removeOverlay?.(overlayRef.current);
|
|
overlayRef.current = null;
|
|
}
|
|
}
|
|
} else {
|
|
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
|
|
// 실패 시 기존 오버레이 제거
|
|
if (overlayRef.current) {
|
|
map.removeOverlay?.(overlayRef.current);
|
|
overlayRef.current = null;
|
|
}
|
|
}
|
|
|
|
// 우측 패널/검색 모달 상태
|
|
setFeatureData(data);
|
|
setIsOpen(false);
|
|
setIsSearchModalOpen(false);
|
|
};
|
|
|
|
const onInit = () => {
|
|
|
|
const map = gisMaps.current;
|
|
if (!map) {
|
|
console.warn("지도가 초기화되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
if (overlayRef.current) {
|
|
map.removeOverlay?.(overlayRef.current);
|
|
overlayRef.current = null;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="bg-container">
|
|
<Gis
|
|
showZoomLevel={true}
|
|
treeData={treeData}
|
|
isSearch={false}
|
|
gisMaps={gisMaps}
|
|
isMiniMap={false}
|
|
searchView={undefined}
|
|
detailView={undefined}
|
|
onFeatureSelect={onFeatureSelect}
|
|
setIsOpen={setIsOpen}
|
|
setFeatureData={setFeatureData}
|
|
featureData={featureData}
|
|
setIsSearchModalOpen={setIsSearchModalOpen}
|
|
isOpen={isOpen}
|
|
isSearchModalOpen={isSearchModalOpen}
|
|
onCheckedLayersChange={undefined}
|
|
imgVisble={false}
|
|
searchBar={
|
|
<SearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} onFeatureFocus={undefined} onInit={onInit} />
|
|
}
|
|
waterDepth={undefined}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Map;
|