267 lines
8.3 KiB
TypeScript
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 };
|
|
};
|