Files
frontend/apps/sn/app/(map-layout)/map/components/exmn/use-exmn-result-map.ts
T

267 lines
8.3 KiB
TypeScript

"use client";
import { beginMapNavigation } from "@/features/map-search/lib/map-navigation";
import {
useCallback,
useEffect,
useRef,
useState,
type RefObject,
} from "react";
import GeoJSON from "ol/format/GeoJSON";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { getExmnMapLayer } from "./exmn-map-api";
import {
EMPTY_EXMN_MAP_FILTERS,
EXMN_RESULT_PPL_LAYER_ID,
EXMN_RESULT_MNHL_LAYER_ID,
type ExmnMapEntryContext,
type ExmnMapFilters,
type ExmnMapJobType,
} from "./exmn-map-types";
type Options = {
gisApp: any;
mapRef: RefObject<any>;
ready: boolean;
treeData: any[];
entry: ExmnMapEntryContext;
pipeChecked: boolean;
manholeChecked: boolean;
pipeEnabled: boolean;
manholeEnabled: boolean;
pipeFilters: ExmnMapFilters;
manholeFilters: ExmnMapFilters;
pipeStyle: any;
manholeStyle: any;
};
type LayerState = {
layer?: VectorLayer<any>;
controller?: AbortController;
sequence: number;
};
/** 전체 조사 레이어와 상세현황 위치 이동을 분리하며 유형별 요청을 취소/무효화한다. */
export const useExmnResultMap = (options: Options) => {
const {
gisApp,
mapRef,
ready,
treeData,
entry,
pipeChecked,
manholeChecked,
pipeEnabled,
manholeEnabled,
pipeFilters,
manholeFilters,
pipeStyle,
manholeStyle,
} = options;
// 지도 객체/요청 수명은 React 렌더링과 분리하고 관로·맨홀을 독립적으로 관리한다.
const layers = useRef<Record<ExmnMapJobType, LayerState>>({
PPL: { sequence: 0 },
MNHL: { sequence: 0 },
});
const fittedContext = useRef("");
const [messages, setMessages] = useState<Record<ExmnMapJobType, string>>({
PPL: "",
MNHL: "",
});
/** 체크 해제/언마운트 시 늦게 도착한 응답도 다시 표시되지 않게 한다. */
const removeLayer = useCallback(
(job: ExmnMapJobType) => {
const state = layers.current[job];
state.sequence++;
state.controller?.abort();
state.controller = undefined;
if (state.layer) {
gisApp.Module.select?.removeLayer?.(state.layer);
mapRef.current?.removeLayer?.(state.layer);
}
state.layer = undefined;
},
[gisApp, mapRef],
);
/** 일반 진입은 전체, 조사관리 지도보기는 선택 사업·처리분구만 조회해 겹친 타 사업 선택을 방지한다. */
const loadLayer = useCallback(
async (job: ExmnMapJobType, filters: ExmnMapFilters) => {
const map = mapRef.current;
if (!map) return;
const navigation = entry.jobType === job && entry.exmnBizId && entry.trtdstId
? beginMapNavigation(map) : null;
removeLayer(job);
const state = layers.current[job];
const sequence = state.sequence;
const controller = new AbortController();
state.controller = controller;
const active = () =>
state.sequence === sequence && !controller.signal.aborted;
setMessages((current) => ({
...current,
[job]: "조사결과를 불러오는 중입니다.",
}));
try {
const response = await getExmnMapLayer(
job,
filters,
entry.exmnBizId && entry.trtdstId
? { exmnBizId: entry.exmnBizId, trtdstId: entry.trtdstId }
: {},
controller.signal,
);
if (!active()) return;
const data = response?.data ?? response;
const features = new GeoJSON().readFeatures(data, {
dataProjection: "EPSG:5179",
featureProjection: gisApp.TO_PROJ,
});
const source = new VectorSource({ features });
const layer = new VectorLayer({
source,
zIndex: job === "PPL" ? 9988 : 9989,
style: job === "PPL" ? pipeStyle : manholeStyle,
});
layer.setProperties({
id:
job === "PPL"
? EXMN_RESULT_PPL_LAYER_ID
: EXMN_RESULT_MNHL_LAYER_ID,
lyrNm: job === "PPL" ? "관로조사" : "맨홀조사",
lyrTyNm: job === "PPL" ? "tb_ms_swage_ppl" : "tb_ms_swage_mhl",
clickPsblYn: "Y",
chcYn: "Y",
});
state.layer = layer;
map.addLayer(layer);
gisApp.Module.select?.addLayer?.(layer);
const missing = Number(data?.missingGeometryCount ?? 0);
const diagnostics = data?.diagnostics ?? {};
const reasons = [
["지역 확인 불가", diagnostics.regionUnresolvedCount],
["시설번호 없음", diagnostics.identifierMissingCount],
["도형 없음", diagnostics.geometryNotFoundCount],
["도형 중복", diagnostics.ambiguousGeometryCount],
]
.filter(([, count]) => Number(count) > 0)
.map(([label, count]) => `${label} ${count}건`)
.join(", ");
const summary = features.length
? `${features.length.toLocaleString()}개 표시`
: "표시할 조사결과가 없습니다.";
const status = missing
? `${summary} · 공간정보 미연결 ${missing}건${reasons ? ` (${reasons})` : ""}`
: summary;
setMessages((current) => ({ ...current, [job]: status }));
if (entry.jobType !== job || !entry.exmnBizId || !entry.trtdstId)
return;
// 위치 조회 실패가 정상적으로 조회된 기본 레이어를 지우지 않도록 별도 처리한다.
try {
const selectedResponse = await getExmnMapLayer(
job,
filters,
{ exmnBizId: entry.exmnBizId, trtdstId: entry.trtdstId },
controller.signal,
);
if (!active()) return;
const selectedData = selectedResponse?.data ?? selectedResponse;
const selectedFeatures = new GeoJSON().readFeatures(selectedData, {
dataProjection: "EPSG:5179",
featureProjection: gisApp.TO_PROJ,
});
if (!selectedFeatures.length) {
setMessages((current) => ({
...current,
[job]: `${status} · 선택한 처리분구에 현재 조건으로 표시할 공간정보가 없습니다.`,
}));
return;
}
const selectedSource = new VectorSource({
features: selectedFeatures,
});
// 별도 강조선을 추가하지 않고 기존 조사 레이어 색상으로 표시한다.
const contextKey = `${entry.exmnBizId}:${entry.trtdstId}:${job}`;
const extent = selectedSource.getExtent();
if (
fittedContext.current !== contextKey &&
extent.every(Number.isFinite) && navigation?.commit(extent)
) {
map
.getView()
.fit(extent, {
duration: 300,
padding: [80, 420, 80, 80],
maxZoom: 18,
constrainResolution: true,
nearest: true,
});
fittedContext.current = contextKey;
}
} catch (error) {
if (active())
setMessages((current) => ({
...current,
[job]: `${status} · 선택 영역 조회 실패: ${error instanceof Error ? error.message : "다시 조회해 주세요."}`,
}));
}
} catch (error) {
if (active())
setMessages((current) => ({
...current,
[job]: `조회 실패: ${error instanceof Error ? error.message : "다시 조회해 주세요."}`,
}));
}
},
[
entry.exmnBizId,
entry.trtdstId,
entry.jobType,
gisApp,
mapRef,
pipeStyle,
manholeStyle,
removeLayer,
],
);
useEffect(() => {
if (ready && pipeChecked)
void loadLayer("PPL", pipeEnabled ? pipeFilters : EMPTY_EXMN_MAP_FILTERS);
else removeLayer("PPL");
return () => removeLayer("PPL");
}, [
ready,
pipeChecked,
pipeEnabled,
pipeFilters,
treeData,
loadLayer,
removeLayer,
]);
useEffect(() => {
if (ready && manholeChecked)
void loadLayer(
"MNHL",
manholeEnabled ? manholeFilters : EMPTY_EXMN_MAP_FILTERS,
);
else removeLayer("MNHL");
return () => removeLayer("MNHL");
}, [
ready,
manholeChecked,
manholeEnabled,
manholeFilters,
treeData,
loadLayer,
removeLayer,
]);
return { removeLayer, messages };
};