Files
frontend/apps/sn/app/(map-layout)/map/components/rdiiResult/useRdiiResultMap.ts
T

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,
};
};