369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
|
|
import GeoJSON from "ol/format/GeoJSON.js";
|
|
import VectorLayer from "ol/layer/Vector.js";
|
|
import VectorSource from "ol/source/Vector.js";
|
|
import Stroke from "ol/style/Stroke.js";
|
|
import Style from "ol/style/Style.js";
|
|
|
|
import { getRawJson } from "@/components/lib/api";
|
|
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
|
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
|
import { getSigunguCenter, moveMapToCenter, sidoCenter } from "@common/business_components/ui/map/searchBar/utils";
|
|
|
|
import {
|
|
RDII_RESULT_OVERLAY_TYPE,
|
|
RDII_RESULT_ROW_SELECT_MESSAGE,
|
|
RDII_RESULT_ROW_SELECT_STORAGE_KEY,
|
|
type RdiiResultMapViewResponse,
|
|
type RdiiResultRow,
|
|
} from "./rdiiResultTypes";
|
|
|
|
type UseRdiiResultMapParams = {
|
|
gisMaps: MutableRefObject<any | null>;
|
|
setFeatureData: (value: any) => void;
|
|
setIsOpen: (value: boolean) => void;
|
|
setIsSearchModalOpen: (value: boolean) => void;
|
|
};
|
|
|
|
const RDII_PIPE_LAYER_ID = "SN_RDII_RESULT_PIPE_LAYER";
|
|
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
|
const PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
|
|
|
|
const pipeStyle = [
|
|
new Style({
|
|
stroke: new Stroke({
|
|
color: "rgba(15, 23, 42, 0.56)",
|
|
width: 9,
|
|
lineCap: "round",
|
|
lineJoin: "round",
|
|
}),
|
|
zIndex: 20020,
|
|
}),
|
|
new Style({
|
|
stroke: new Stroke({
|
|
color: "rgba(255, 255, 255, 0.98)",
|
|
width: 5,
|
|
lineCap: "round",
|
|
lineJoin: "round",
|
|
}),
|
|
zIndex: 20021,
|
|
}),
|
|
new Style({
|
|
stroke: new Stroke({
|
|
color: "rgba(59, 130, 246, 0.98)",
|
|
width: 2.5,
|
|
lineCap: "round",
|
|
lineJoin: "round",
|
|
}),
|
|
zIndex: 20022,
|
|
}),
|
|
];
|
|
|
|
const getFeatureCollection = (payload: any) => {
|
|
if (payload?.type === "FeatureCollection") return payload;
|
|
if (payload?.data?.type === "FeatureCollection") return payload.data;
|
|
return { type: "FeatureCollection", features: [] };
|
|
};
|
|
|
|
const normalizeProjectionName = (value: string) => {
|
|
const trimmed = String(value ?? "").trim();
|
|
if (!trimmed) return "EPSG:4326";
|
|
if (trimmed.startsWith("EPSG:")) return trimmed;
|
|
const match = trimmed.match(/EPSG[:/]{1,2}(\d{4,5})/i) || trimmed.match(/(\d{4,5})$/);
|
|
return match?.[1] ? `EPSG:${match[1]}` : trimmed;
|
|
};
|
|
|
|
const resolveFeatureCollectionProjection = (payload: any) => {
|
|
const featureCollection = getFeatureCollection(payload);
|
|
const crsName = String(featureCollection?.crs?.properties?.name ?? "").trim();
|
|
return normalizeProjectionName(crsName || GisApp.TO_PROJ);
|
|
};
|
|
|
|
const getOverlayFlag = (data: any) =>
|
|
String(
|
|
data?.get?.("analysisOverlayType") ??
|
|
data?.values_?.analysisOverlayType ??
|
|
data?.analysisOverlayType ??
|
|
"",
|
|
).trim();
|
|
|
|
const getOverlayBasePplMngNo = (data: any) =>
|
|
String(
|
|
data?.get?.("fcltMngNo") ??
|
|
data?.values_?.fcltMngNo ??
|
|
data?.values_?.fclt_mng_no ??
|
|
data?.fcltMngNo ??
|
|
data?.fclt_mng_no ??
|
|
"",
|
|
).trim();
|
|
|
|
const getOverlayAnalysisPplMngNo = (data: any) =>
|
|
String(
|
|
data?.get?.("pplMngNo") ??
|
|
data?.values_?.pplMngNo ??
|
|
data?.pplMngNo ??
|
|
"",
|
|
).trim();
|
|
|
|
const getOverlayAnalysisSn = (data: any) =>
|
|
Number(
|
|
data?.get?.("rainInfwtAnlsSn") ??
|
|
data?.values_?.rainInfwtAnlsSn ??
|
|
data?.rainInfwtAnlsSn ??
|
|
0,
|
|
);
|
|
|
|
const getOverlayAplcnYm = (data: any) =>
|
|
String(
|
|
data?.get?.("aplcnYm") ??
|
|
data?.values_?.aplcnYm ??
|
|
data?.aplcnYm ??
|
|
"",
|
|
).trim();
|
|
|
|
const getOverlayAnalysisInfo = (data: any) =>
|
|
String(
|
|
data?.get?.("analysisInfo") ??
|
|
data?.values_?.analysisInfo ??
|
|
data?.analysisInfo ??
|
|
"",
|
|
).trim();
|
|
|
|
export const useRdiiResultMap = ({
|
|
gisMaps,
|
|
setFeatureData,
|
|
setIsOpen,
|
|
setIsSearchModalOpen,
|
|
}: UseRdiiResultMapParams) => {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const activeResultRowRef = useRef<RdiiResultRow | null>(null);
|
|
const pipeLayerRef = useRef<VectorLayer<any> | null>(null);
|
|
const pipeSourceRef = useRef<VectorSource | null>(null);
|
|
const mapViewCacheRef = useRef<Map<number, RdiiResultMapViewResponse>>(new Map());
|
|
const [activeMapView, setActiveMapView] = useState<RdiiResultMapViewResponse | null>(null);
|
|
|
|
const clearLayers = useCallback(() => {
|
|
pipeSourceRef.current?.clear?.();
|
|
pipeLayerRef.current?.changed?.();
|
|
gisMaps.current?.renderSync?.();
|
|
}, [gisMaps]);
|
|
|
|
const ensurePipeLayer = useCallback(() => {
|
|
const map = gisMaps.current;
|
|
if (!map) return null;
|
|
|
|
if (pipeLayerRef.current && pipeSourceRef.current) {
|
|
return pipeSourceRef.current;
|
|
}
|
|
|
|
const source = new VectorSource();
|
|
const layer = new VectorLayer({
|
|
source,
|
|
zIndex: 20019,
|
|
});
|
|
|
|
layer.set("id", RDII_PIPE_LAYER_ID);
|
|
layer.set("lyrNm", "RDII 결과관로");
|
|
layer.set("lyrTyNm", "tb_ms_swage_ppl");
|
|
layer.set("clickPsblYn", "Y");
|
|
layer.setVisible?.(true);
|
|
map.addLayer(layer);
|
|
|
|
pipeLayerRef.current = layer;
|
|
pipeSourceRef.current = source;
|
|
return source;
|
|
}, [gisMaps]);
|
|
|
|
const fitToRenderedFeatures = useCallback(() => {
|
|
const map = gisMaps.current;
|
|
const pipeExtent = pipeSourceRef.current?.getExtent?.();
|
|
if (!map || !pipeExtent || pipeExtent.some((value: number) => !Number.isFinite(value))) {
|
|
return;
|
|
}
|
|
|
|
map.getView?.().fit?.(pipeExtent, {
|
|
duration: 350,
|
|
padding: DEFAULT_RESULT_FIT_PADDING,
|
|
maxZoom: 16,
|
|
constrainResolution: true,
|
|
nearest: true,
|
|
});
|
|
}, [gisMaps]);
|
|
|
|
const moveByRegionCodes = useCallback(
|
|
(ctpvCd?: string, sggCd?: string) => {
|
|
const normalizedSggCd = String(sggCd ?? "").trim();
|
|
if (normalizedSggCd && normalizedSggCd !== "0") {
|
|
const sigunguCenter = getSigunguCenter(normalizedSggCd);
|
|
if (sigunguCenter) {
|
|
moveMapToCenter(gisMaps, sigunguCenter, 12);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const normalizedCtpvCd = String(ctpvCd ?? "").trim();
|
|
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
|
|
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
|
|
if (sidoCenterCoordinate) {
|
|
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
[gisMaps],
|
|
);
|
|
|
|
const moveToStableFocus = useCallback(
|
|
async (mapView: RdiiResultMapViewResponse) => {
|
|
const activeRow = activeResultRowRef.current;
|
|
const movedByRegion = moveByRegionCodes(
|
|
activeRow?.ctpvCd ?? mapView.analysis?.ctpvCd,
|
|
activeRow?.sggCd ?? mapView.analysis?.sggCd,
|
|
);
|
|
if (movedByRegion) return;
|
|
|
|
fitToRenderedFeatures();
|
|
},
|
|
[fitToRenderedFeatures, moveByRegionCodes],
|
|
);
|
|
|
|
const renderMapView = useCallback(
|
|
async (mapView: RdiiResultMapViewResponse) => {
|
|
const pipeSource = ensurePipeLayer();
|
|
if (!pipeSource) return;
|
|
|
|
clearLayers();
|
|
|
|
const pipeDataProjection = resolveFeatureCollectionProjection(mapView.pipeGeometry);
|
|
const pipeFeatures = new GeoJSON().readFeatures(getFeatureCollection(mapView.pipeGeometry), {
|
|
dataProjection: pipeDataProjection,
|
|
featureProjection: GisApp.TO_PROJ,
|
|
});
|
|
pipeFeatures.forEach((feature: any) => {
|
|
feature.setStyle(pipeStyle);
|
|
});
|
|
pipeSource.addFeatures(pipeFeatures);
|
|
pipeSource.changed?.();
|
|
pipeLayerRef.current?.setVisible?.(true);
|
|
pipeLayerRef.current?.changed?.();
|
|
gisMaps.current?.renderSync?.();
|
|
|
|
await moveToStableFocus(mapView);
|
|
},
|
|
[clearLayers, ensurePipeLayer, gisMaps, moveToStableFocus],
|
|
);
|
|
|
|
const handleRowSelect = useCallback(
|
|
async (row: RdiiResultRow) => {
|
|
try {
|
|
activeResultRowRef.current = row;
|
|
setFeatureData(null);
|
|
setIsOpen(false);
|
|
setIsSearchModalOpen(false);
|
|
|
|
if (typeof window !== "undefined") {
|
|
window.dispatchEvent(
|
|
new CustomEvent(PROC_SEARCH_EVENT, {
|
|
detail: {
|
|
ctpvCd: row.ctpvCd ?? "0",
|
|
sggCd: row.sggCd ?? "0",
|
|
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
const cachedMapView = mapViewCacheRef.current.get(row.rainInfwtAnlsSn);
|
|
if (cachedMapView) {
|
|
setActiveMapView(cachedMapView);
|
|
void renderMapView(cachedMapView);
|
|
return;
|
|
}
|
|
|
|
const payload = await getRawJson(`/sewerage-service/api/v1/rdiiAnlsRslt/mapView/${row.rainInfwtAnlsSn}`);
|
|
const mapView = (payload?.data ?? payload) as RdiiResultMapViewResponse;
|
|
mapViewCacheRef.current.set(row.rainInfwtAnlsSn, mapView);
|
|
setActiveMapView(mapView);
|
|
void renderMapView(mapView);
|
|
} catch (error) {
|
|
console.error("RDII 분석결과 지도 연계 중 오류:", error);
|
|
alert("RDII 분석결과를 지도에 연계하지 못했습니다.");
|
|
}
|
|
},
|
|
[alert, renderMapView, setFeatureData, setIsOpen, setIsSearchModalOpen],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handlePopupMessage = (event: MessageEvent) => {
|
|
if (event.origin !== window.location.origin) return;
|
|
if (event.data?.type !== RDII_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
|
|
void handleRowSelect(event.data.row as RdiiResultRow);
|
|
};
|
|
|
|
const handleStorageEvent = (event: StorageEvent) => {
|
|
if (event.key !== RDII_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
|
|
|
|
try {
|
|
const payload = JSON.parse(event.newValue) as { row?: RdiiResultRow };
|
|
if (!payload?.row) return;
|
|
void handleRowSelect(payload.row);
|
|
} catch (error) {
|
|
console.warn("RDII 분석결과 storage 동기화 값을 해석하지 못했습니다.", error);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("message", handlePopupMessage);
|
|
window.addEventListener("storage", handleStorageEvent);
|
|
return () => {
|
|
window.removeEventListener("message", handlePopupMessage);
|
|
window.removeEventListener("storage", handleStorageEvent);
|
|
};
|
|
}, [handleRowSelect]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
clearLayers();
|
|
};
|
|
}, [clearLayers]);
|
|
|
|
const handleOverlayFeatureSelect = useCallback((data: any) => {
|
|
if (getOverlayFlag(data) !== RDII_RESULT_OVERLAY_TYPE) {
|
|
return false;
|
|
}
|
|
|
|
const overlayAnalysisSn = getOverlayAnalysisSn(data);
|
|
const basePplMngNo = getOverlayBasePplMngNo(data);
|
|
const analysisPplMngNo = getOverlayAnalysisPplMngNo(data);
|
|
const currentRow = activeResultRowRef.current;
|
|
const popupWindow = openWindowPopup("rdiiChart", {
|
|
pplMngNo: basePplMngNo || analysisPplMngNo.replace(/^L/, ""),
|
|
rainInfwtAnlsSn: overlayAnalysisSn || currentRow?.rainInfwtAnlsSn || 0,
|
|
aplcnYm: getOverlayAplcnYm(data) || currentRow?.aplcnYm || activeMapView?.analysis?.aplcnYm || "",
|
|
analysisInfo:
|
|
getOverlayAnalysisInfo(data) ||
|
|
currentRow?.anlsInfo ||
|
|
activeMapView?.analysis?.anlsInfo ||
|
|
"",
|
|
resultOnly: "Y",
|
|
});
|
|
|
|
if (!popupWindow) {
|
|
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
|
|
}
|
|
|
|
return true;
|
|
}, [activeMapView, alert]);
|
|
|
|
return {
|
|
activeMapView,
|
|
clearLayers,
|
|
handleOverlayFeatureSelect,
|
|
};
|
|
};
|