Files
frontend/apps/fo/app/pages/(map-layout)/map/page.tsx
T

328 lines
10 KiB
TypeScript

"use client";
import { SITE_ID } from "@/src/shared/config/env";
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",
"X-Site-Id": SITE_ID,
},
});
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;