Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cc4f58076 | ||
|
|
3f07739756 |
@@ -18,10 +18,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Gis from "@common/business_components/ui/map/Gis";
|
|
||||||
import { getData, getRawJson } from "../../components/lib/api";
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import GeoJSON from "ol/format/GeoJSON.js";
|
import GeoJSON from "ol/format/GeoJSON.js";
|
||||||
import Point from "ol/geom/Point.js";
|
import Point from "ol/geom/Point.js";
|
||||||
import VectorLayer from "ol/layer/Vector.js";
|
import VectorLayer from "ol/layer/Vector.js";
|
||||||
@@ -29,35 +28,44 @@ import { unByKey } from "ol/Observable.js";
|
|||||||
import VectorSource from "ol/source/Vector.js";
|
import VectorSource from "ol/source/Vector.js";
|
||||||
import Stroke from "ol/style/Stroke.js";
|
import Stroke from "ol/style/Stroke.js";
|
||||||
import Style from "ol/style/Style.js";
|
import Style from "ol/style/Style.js";
|
||||||
import SearchModal from "./searchModal/searchModal";
|
|
||||||
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||||
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
|
import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||||
|
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
||||||
|
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||||
|
import Gis from "@common/business_components/ui/map/Gis";
|
||||||
|
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||||
import { Button } from "@common/components";
|
import { Button } from "@common/components";
|
||||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
|
||||||
import useStore from "@/store/useStore";
|
import { getData, getRawJson } from "../../components/lib/api";
|
||||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
|
||||||
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
|
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
|
||||||
import type { PipeAnalysisDetailRow } from "./components/pipeAnalysis/pipeAnalysisUtils";
|
|
||||||
import {
|
import {
|
||||||
|
PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE,
|
||||||
|
type PipeAnalysisDetailRow,
|
||||||
|
} from "./components/pipeAnalysis/pipeAnalysisUtils";
|
||||||
|
import {
|
||||||
|
AGED_PIPE_DEFAULT_FILTERS,
|
||||||
AGED_PIPE_EXCEL_HEADER,
|
AGED_PIPE_EXCEL_HEADER,
|
||||||
AGED_PIPE_MIN_ZOOM,
|
|
||||||
AGED_PIPE_LAYER_TYPE,
|
AGED_PIPE_LAYER_TYPE,
|
||||||
|
AGED_PIPE_MIN_ZOOM,
|
||||||
AGED_PIPE_PAGE_SIZE,
|
AGED_PIPE_PAGE_SIZE,
|
||||||
|
type AgedPipeFilters,
|
||||||
|
type AgedPipeRow,
|
||||||
buildAgedPipeCqlFilters,
|
buildAgedPipeCqlFilters,
|
||||||
isAgedPipeLayerNode,
|
isAgedPipeLayerNode,
|
||||||
matchesAgedPipeFilters,
|
matchesAgedPipeFilters,
|
||||||
normalizeAgedPipeFilters,
|
normalizeAgedPipeFilters,
|
||||||
toDisplayPipeRow,
|
toDisplayPipeRow,
|
||||||
validateElapsedYears,
|
validateElapsedYears,
|
||||||
type AgedPipeFilters,
|
|
||||||
type AgedPipeRow,
|
|
||||||
AGED_PIPE_DEFAULT_FILTERS,
|
|
||||||
} from "./components/agedPipe/agedPipeUtils";
|
} from "./components/agedPipe/agedPipeUtils";
|
||||||
import { PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE } from "./components/pipeAnalysis/pipeAnalysisUtils";
|
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
||||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
import SearchModal from "./searchModal/searchModal";
|
||||||
import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
import { WATER_DEPTH_LAYER_TYPES, WaterDepth } from "./waterDepth/waterDepth";
|
||||||
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
|
|
||||||
|
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||||
|
import useStore from "@/store/useStore";
|
||||||
|
|
||||||
const AnalysisPopupMenu = dynamic(
|
const AnalysisPopupMenu = dynamic(
|
||||||
() => import("./components/analysisMenu/AnalysisPopupMenu"),
|
() => import("./components/analysisMenu/AnalysisPopupMenu"),
|
||||||
@@ -79,18 +87,27 @@ const SOUTH_KOREA_VIEW_EXTENT: [number, number, number, number] = [
|
|||||||
// 유하방향은 GeoServer WMS/WFS 레이어가 아니라 BE 보정 GeoJSON API로 그리는 가상 레이어이다.
|
// 유하방향은 GeoServer WMS/WFS 레이어가 아니라 BE 보정 GeoJSON API로 그리는 가상 레이어이다.
|
||||||
const FLOW_DIRECTION_LAYER_TYPE = "tb_sn_swage_ppl_flow_direction";
|
const FLOW_DIRECTION_LAYER_TYPE = "tb_sn_swage_ppl_flow_direction";
|
||||||
const FLOW_DIRECTION_LAYER_NAME = "유하방향";
|
const FLOW_DIRECTION_LAYER_NAME = "유하방향";
|
||||||
|
const FLOW_DIRECTION_PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||||
const FLOW_DIRECTION_SRS = "EPSG:5179";
|
const FLOW_DIRECTION_SRS = "EPSG:5179";
|
||||||
const FLOW_DIRECTION_DEFAULT_MIN_ZOOM = 13;
|
const FLOW_DIRECTION_DEFAULT_MIN_ZOOM = 13;
|
||||||
const FLOW_DIRECTION_DEFAULT_LIMIT = 5000;
|
|
||||||
const FLOW_DIRECTION_DEFAULT_COLOR = "rgba(40, 90, 180, 0.84)";
|
const FLOW_DIRECTION_DEFAULT_COLOR = "rgba(40, 90, 180, 0.84)";
|
||||||
|
const FLOW_DIRECTION_NETWORK_CORRECTION_COLOR = "rgba(0, 168, 132, 0.95)";
|
||||||
const FLOW_DIRECTION_BBOX_BUFFER_RATIO = 0.35;
|
const FLOW_DIRECTION_BBOX_BUFFER_RATIO = 0.35;
|
||||||
const FLOW_DIRECTION_CACHE_MAX_FEATURES = 10000;
|
|
||||||
|
|
||||||
type FlowDirectionUsageStyle = {
|
type FlowDirectionUsageStyle = {
|
||||||
color: string;
|
color: string;
|
||||||
lineColor: string;
|
lineColor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FlowDirectionMode = "PIPE_START_END" | "PIPE_DEPTH";
|
||||||
|
|
||||||
|
// /map은 관로 깊이 기준, /map2는 관로 원천 geometry 시점->종점 기준만 유지한다. 두 화면 모두 BBOX 조회만 사용한다.
|
||||||
|
const getFlowDirectionModeByPathname = (pathname: string | null): FlowDirectionMode => {
|
||||||
|
const normalizedPathname = String(pathname ?? "").replace(/\/$/, "");
|
||||||
|
if (normalizedPathname.endsWith("/map2")) return "PIPE_START_END";
|
||||||
|
return "PIPE_DEPTH";
|
||||||
|
};
|
||||||
|
|
||||||
const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||||
const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [50, 50, 50, 50];
|
const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [50, 50, 50, 50];
|
||||||
|
|
||||||
@@ -141,15 +158,19 @@ const normalizeFlowDirectionLayerInfos = (data: any[]): any[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 레이어 설정의 최소 표출 줌을 읽되, 값이 없으면 유하방향 기본 줌 기준을 적용한다.
|
// 레이어 설정의 최소 표출 줌을 읽되, 값이 없으면 유하방향 기본 줌 기준을 적용한다.
|
||||||
|
const getFiniteNumber = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined || String(value).trim() === "") return null;
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
};
|
||||||
|
|
||||||
const getFlowDirectionMinZoom = (layerInfo: any) => {
|
const getFlowDirectionMinZoom = (layerInfo: any) => {
|
||||||
const minZoom = Number(layerInfo?.minViewSize ?? layerInfo?.minZoom);
|
return getFiniteNumber(layerInfo?.minViewSize ?? layerInfo?.minZoom) ?? FLOW_DIRECTION_DEFAULT_MIN_ZOOM;
|
||||||
return Number.isFinite(minZoom) ? minZoom : FLOW_DIRECTION_DEFAULT_MIN_ZOOM;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 유하방향 화살표는 관로 위에 얹는 보조 레이어라 레이어 설정의 drawOrdr를 우선 사용하고 없으면 높은 zIndex를 준다.
|
// 유하방향 화살표는 관로 위에 얹는 보조 레이어라 레이어 설정의 drawOrdr를 우선 사용하고 없으면 높은 zIndex를 준다.
|
||||||
const getFlowDirectionZIndex = (layerInfo: any) => {
|
const getFlowDirectionZIndex = (layerInfo: any) => {
|
||||||
const drawOrder = Number(layerInfo?.drawOrdr);
|
return getFiniteNumber(layerInfo?.drawOrdr) ?? 9998;
|
||||||
return Number.isFinite(drawOrder) ? drawOrder : 9998;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// BE 응답이 FeatureCollection 그대로 오거나 공통 응답 wrapper의 data 안에 들어오는 두 계약을 모두 처리한다.
|
// BE 응답이 FeatureCollection 그대로 오거나 공통 응답 wrapper의 data 안에 들어오는 두 계약을 모두 처리한다.
|
||||||
@@ -366,8 +387,19 @@ const createFlowDirectionStyle = (
|
|||||||
const usageCode = String(feature.get("usgCd") ?? feature.get("usg_cd") ?? "").trim();
|
const usageCode = String(feature.get("usgCd") ?? feature.get("usg_cd") ?? "").trim();
|
||||||
const usageStyle = usageStyleMap[usageCode];
|
const usageStyle = usageStyleMap[usageCode];
|
||||||
const baseColor = usageStyle?.color ?? FLOW_DIRECTION_DEFAULT_COLOR;
|
const baseColor = usageStyle?.color ?? FLOW_DIRECTION_DEFAULT_COLOR;
|
||||||
|
const isNetworkCorrected = String(
|
||||||
|
feature.get("networkCorrectionYn") ??
|
||||||
|
feature.get("network_correction_yn") ??
|
||||||
|
"",
|
||||||
|
).toUpperCase() === "Y";
|
||||||
|
|
||||||
const styleByConfidence: Record<string, { color: string; lineColor: string; scale: number; width: number }> = {
|
const styleByConfidence: Record<string, { color: string; lineColor: string; scale: number; width: number; lineDash?: number[] }> = {
|
||||||
|
HIGH: {
|
||||||
|
color: baseColor,
|
||||||
|
lineColor: usageStyle?.lineColor ?? withAlpha(baseColor, 0.42),
|
||||||
|
scale: 1,
|
||||||
|
width: 1.3,
|
||||||
|
},
|
||||||
MEDIUM: {
|
MEDIUM: {
|
||||||
color: baseColor,
|
color: baseColor,
|
||||||
lineColor: usageStyle?.lineColor ?? withAlpha(baseColor, 0.42),
|
lineColor: usageStyle?.lineColor ?? withAlpha(baseColor, 0.42),
|
||||||
@@ -381,12 +413,21 @@ const createFlowDirectionStyle = (
|
|||||||
width: 1.1,
|
width: 1.1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const style = styleByConfidence[confidence] ?? styleByConfidence.LOW!;
|
const style = isNetworkCorrected
|
||||||
|
? {
|
||||||
|
color: FLOW_DIRECTION_NETWORK_CORRECTION_COLOR,
|
||||||
|
lineColor: withAlpha(FLOW_DIRECTION_NETWORK_CORRECTION_COLOR, 0.5),
|
||||||
|
scale: 1.08,
|
||||||
|
width: 2,
|
||||||
|
lineDash: [8, 4],
|
||||||
|
}
|
||||||
|
: styleByConfidence[confidence] ?? styleByConfidence.LOW!;
|
||||||
const styles = [
|
const styles = [
|
||||||
new Style({
|
new Style({
|
||||||
stroke: new Stroke({
|
stroke: new Stroke({
|
||||||
color: style.lineColor,
|
color: style.lineColor,
|
||||||
width: style.width,
|
width: style.width,
|
||||||
|
lineDash: style.lineDash,
|
||||||
lineCap: "round",
|
lineCap: "round",
|
||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
}),
|
}),
|
||||||
@@ -435,8 +476,8 @@ const extractUsageCodesFromLayerInfo = (layerInfo: any) => {
|
|||||||
|
|
||||||
// 현재 줌에서 실제 표출 가능한 관로 레이어인지 확인해 숨겨진 관종의 유하방향 요청을 막는다.
|
// 현재 줌에서 실제 표출 가능한 관로 레이어인지 확인해 숨겨진 관종의 유하방향 요청을 막는다.
|
||||||
const isLayerVisibleAtZoom = (layerInfo: any, zoom: number) => {
|
const isLayerVisibleAtZoom = (layerInfo: any, zoom: number) => {
|
||||||
const minZoom = Number(layerInfo?.minViewSize ?? layerInfo?.minZoom);
|
const minZoom = getFiniteNumber(layerInfo?.minViewSize ?? layerInfo?.minZoom);
|
||||||
return !Number.isFinite(minZoom) || zoom >= minZoom;
|
return minZoom === null || zoom >= minZoom;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 현재 체크되어 화면에 보이는 관로 레이어의 관종 코드와 선 색상을 유하방향 조회/스타일 기준으로 변환한다.
|
// 현재 체크되어 화면에 보이는 관로 레이어의 관종 코드와 선 색상을 유하방향 조회/스타일 기준으로 변환한다.
|
||||||
@@ -446,6 +487,7 @@ const extractCheckedPipeUsageStyles = (checkedNodesData: any[], zoom: number) =>
|
|||||||
checkedNodesData.forEach((node) => {
|
checkedNodesData.forEach((node) => {
|
||||||
const data = node?.data ?? node;
|
const data = node?.data ?? node;
|
||||||
if (!data || isFlowDirectionLayerInfo(data)) return;
|
if (!data || isFlowDirectionLayerInfo(data)) return;
|
||||||
|
if (getLayerTypeName(data) !== FLOW_DIRECTION_PIPE_LAYER_TYPE) return;
|
||||||
if (!isLayerVisibleAtZoom(data, zoom)) return;
|
if (!isLayerVisibleAtZoom(data, zoom)) return;
|
||||||
|
|
||||||
const usageCodes = extractUsageCodesFromLayerInfo(data);
|
const usageCodes = extractUsageCodesFromLayerInfo(data);
|
||||||
@@ -507,6 +549,8 @@ const appendAgedPipeControlNode = (data: any[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Map = () => {
|
const Map = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const flowDirectionMode = useMemo(() => getFlowDirectionModeByPathname(pathname), [pathname]);
|
||||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||||
|
|
||||||
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
||||||
@@ -550,9 +594,13 @@ const Map = () => {
|
|||||||
// 같은 관로가 겹치는 BBOX에서 반복 조회될 수 있으므로 고유키와 삽입 순서를 캐시로 관리한다.
|
// 같은 관로가 겹치는 BBOX에서 반복 조회될 수 있으므로 고유키와 삽입 순서를 캐시로 관리한다.
|
||||||
const flowDirectionFeatureKeySetRef = useRef<Set<string>>(new Set());
|
const flowDirectionFeatureKeySetRef = useRef<Set<string>>(new Set());
|
||||||
const flowDirectionFeatureKeyQueueRef = useRef<string[]>([]);
|
const flowDirectionFeatureKeyQueueRef = useRef<string[]>([]);
|
||||||
|
// 관종 체크 변경 시 다른 관종 유하방향을 유지하기 위해 현재 source에 남겨둘 관종 코드를 별도로 추적한다.
|
||||||
|
const flowDirectionVisibleUsageCodesRef = useRef<Set<string>>(new Set());
|
||||||
|
// 관종 체크 변경으로 새로 켜진 usgCd만 다음 조회 대상으로 넘겨 기존 관종 재요청을 막는다.
|
||||||
|
const flowDirectionSelectionRequestUsageCodesRef = useRef<string[] | null>(null);
|
||||||
// 체크 해제나 재조회 중 늦게 도착한 이전 API 응답이 레이어를 다시 그리지 못하도록 요청 순번을 관리한다.
|
// 체크 해제나 재조회 중 늦게 도착한 이전 API 응답이 레이어를 다시 그리지 못하도록 요청 순번을 관리한다.
|
||||||
const flowDirectionRequestSeqRef = useRef(0);
|
const flowDirectionRequestSeqRef = useRef(0);
|
||||||
// 지도 이동이 연속으로 발생하면 이전 bbox 요청을 중단해 늦은 응답과 불필요한 네트워크 대기를 줄인다.
|
// 지도 이동이 연속으로 발생하면 이전 유하방향 요청을 중단해 늦은 응답과 불필요한 네트워크 대기를 줄인다.
|
||||||
const flowDirectionAbortControllerRef = useRef<AbortController | null>(null);
|
const flowDirectionAbortControllerRef = useRef<AbortController | null>(null);
|
||||||
// 같은 장애 상황에서 alert가 반복 노출되지 않도록 실패 안내 여부를 기억한다.
|
// 같은 장애 상황에서 alert가 반복 노출되지 않도록 실패 안내 여부를 기억한다.
|
||||||
const flowDirectionFailureAlertedRef = useRef(false);
|
const flowDirectionFailureAlertedRef = useRef(false);
|
||||||
@@ -630,6 +678,7 @@ const Map = () => {
|
|||||||
flowDirectionSourceRef.current = null;
|
flowDirectionSourceRef.current = null;
|
||||||
flowDirectionFeatureKeySetRef.current.clear();
|
flowDirectionFeatureKeySetRef.current.clear();
|
||||||
flowDirectionFeatureKeyQueueRef.current = [];
|
flowDirectionFeatureKeyQueueRef.current = [];
|
||||||
|
flowDirectionVisibleUsageCodesRef.current.clear();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 유하방향 cache/source만 비워 관종 변경처럼 조건이 달라진 경우 이전 조건의 피처가 남지 않게 한다.
|
// 유하방향 cache/source만 비워 관종 변경처럼 조건이 달라진 경우 이전 조건의 피처가 남지 않게 한다.
|
||||||
@@ -637,8 +686,46 @@ const Map = () => {
|
|||||||
flowDirectionSourceRef.current?.clear();
|
flowDirectionSourceRef.current?.clear();
|
||||||
flowDirectionFeatureKeySetRef.current.clear();
|
flowDirectionFeatureKeySetRef.current.clear();
|
||||||
flowDirectionFeatureKeyQueueRef.current = [];
|
flowDirectionFeatureKeyQueueRef.current = [];
|
||||||
|
flowDirectionVisibleUsageCodesRef.current.clear();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 체크 해제된 관종의 유하방향 feature/cache만 제거하고, 새로 체크된 관종 코드만 다음 요청 대상으로 돌려준다.
|
||||||
|
const retainFlowDirectionUsageCodes = useCallback((usageCodes: string[]) => {
|
||||||
|
const nextUsageCodes = new Set(usageCodes);
|
||||||
|
const addedUsageCodes = usageCodes.filter((usageCode) =>
|
||||||
|
!flowDirectionVisibleUsageCodesRef.current.has(usageCode),
|
||||||
|
);
|
||||||
|
const source = flowDirectionSourceRef.current;
|
||||||
|
if (source) {
|
||||||
|
source.getFeatures().forEach((feature: any) => {
|
||||||
|
const usageCode = String(feature.get("usgCd") ?? feature.get("usg_cd") ?? "").trim();
|
||||||
|
if (nextUsageCodes.has(usageCode)) return;
|
||||||
|
|
||||||
|
const featureKey = feature.get("flowDirectionCacheKey");
|
||||||
|
if (featureKey) {
|
||||||
|
flowDirectionFeatureKeySetRef.current.delete(String(featureKey));
|
||||||
|
}
|
||||||
|
source.removeFeature(feature);
|
||||||
|
});
|
||||||
|
flowDirectionFeatureKeyQueueRef.current = flowDirectionFeatureKeyQueueRef.current.filter((featureKey) =>
|
||||||
|
flowDirectionFeatureKeySetRef.current.has(featureKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
flowDirectionVisibleUsageCodesRef.current = new Set(
|
||||||
|
Array.from(flowDirectionVisibleUsageCodesRef.current).filter((usageCode) =>
|
||||||
|
nextUsageCodes.has(usageCode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return addedUsageCodes;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!flowDirectionCheckedRef.current) return;
|
||||||
|
clearFlowDirectionFeatureCache();
|
||||||
|
flowDirectionSelectionRequestUsageCodesRef.current = null;
|
||||||
|
setFlowDirectionSelectionVersion((version) => version + 1);
|
||||||
|
}, [clearFlowDirectionFeatureCache, flowDirectionMode]);
|
||||||
|
|
||||||
// 유하방향 레이어는 하나만 유지하고 source에 피처를 누적한다.
|
// 유하방향 레이어는 하나만 유지하고 source에 피처를 누적한다.
|
||||||
const ensureFlowDirectionLayer = useCallback((layerInfo: any) => {
|
const ensureFlowDirectionLayer = useCallback((layerInfo: any) => {
|
||||||
const map = gisMaps.current;
|
const map = gisMaps.current;
|
||||||
@@ -704,23 +791,11 @@ const Map = () => {
|
|||||||
source.addFeatures(nextFeatures);
|
source.addFeatures(nextFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (flowDirectionFeatureKeyQueueRef.current.length > FLOW_DIRECTION_CACHE_MAX_FEATURES) {
|
|
||||||
const staleKey = flowDirectionFeatureKeyQueueRef.current.shift();
|
|
||||||
if (!staleKey) break;
|
|
||||||
|
|
||||||
const staleFeature = source.getFeatures().find((feature: any) =>
|
|
||||||
feature.get("flowDirectionCacheKey") === staleKey,
|
|
||||||
);
|
|
||||||
if (staleFeature) {
|
|
||||||
source.removeFeature(staleFeature);
|
|
||||||
}
|
|
||||||
flowDirectionFeatureKeySetRef.current.delete(staleKey);
|
|
||||||
}
|
|
||||||
}, [ensureFlowDirectionLayer]);
|
}, [ensureFlowDirectionLayer]);
|
||||||
|
|
||||||
// 현재 지도 BBOX와 체크된 관종 필터로 유하방향 GeoJSON을 조회하고, 최신 요청만 지도에 반영한다.
|
// 현재 지도 BBOX와 체크된 관종별 필터로 유하방향 GeoJSON을 각각 조회하고, 최신 요청만 지도에 반영한다.
|
||||||
const loadFlowDirectionLayer = useCallback(
|
const loadFlowDirectionLayer = useCallback(
|
||||||
async (layerInfo: any) => {
|
async (layerInfo: any, requestOnlyUsageCodes?: string[]) => {
|
||||||
const map = gisMaps.current;
|
const map = gisMaps.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
@@ -748,45 +823,113 @@ const Map = () => {
|
|||||||
removeFlowDirectionLayer();
|
removeFlowDirectionLayer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
retainFlowDirectionUsageCodes(usageCodes);
|
||||||
|
|
||||||
|
const usageCodeSet = new Set(usageCodes);
|
||||||
|
const requestUsageCodes = requestOnlyUsageCodes
|
||||||
|
? requestOnlyUsageCodes.filter((usageCode) => usageCodeSet.has(usageCode))
|
||||||
|
: usageCodes;
|
||||||
|
const isPartialUsageRequest = Array.isArray(requestOnlyUsageCodes);
|
||||||
|
if (requestUsageCodes.length === 0) {
|
||||||
|
if (flowDirectionAbortControllerRef.current === abortController) {
|
||||||
|
flowDirectionAbortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const screenExtent = view.calculateExtent(size);
|
const screenExtent = view.calculateExtent(size);
|
||||||
const requestExtent = expandFlowDirectionExtent(screenExtent);
|
const requestExtent = expandFlowDirectionExtent(screenExtent);
|
||||||
const screenBbox = formatFlowDirectionBbox(screenExtent);
|
const screenBbox = formatFlowDirectionBbox(screenExtent);
|
||||||
const requestBbox = formatFlowDirectionBbox(requestExtent);
|
const requestBbox = formatFlowDirectionBbox(requestExtent);
|
||||||
const params = new URLSearchParams({
|
const baseParams = new URLSearchParams({
|
||||||
bbox: requestBbox,
|
|
||||||
srs: FLOW_DIRECTION_SRS,
|
srs: FLOW_DIRECTION_SRS,
|
||||||
limit: String(FLOW_DIRECTION_DEFAULT_LIMIT),
|
directionMode: flowDirectionMode,
|
||||||
usgCd: usageCodes.join(","),
|
searchStrategy: "BBOX",
|
||||||
|
bbox: requestBbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await getRawJson(
|
const settledResponses = await Promise.allSettled(requestUsageCodes.map(async (usageCode) => {
|
||||||
`/sewerage-service/api/v1/map/flowDirectionGeometry?${params.toString()}`,
|
const params = new URLSearchParams(baseParams);
|
||||||
{ signal: abortController.signal },
|
params.set("usgCd", usageCode);
|
||||||
);
|
const payload = await getRawJson(
|
||||||
|
`/sewerage-service/api/v1/map/flowDirectionGeometry?${params.toString()}`,
|
||||||
|
{ signal: abortController.signal },
|
||||||
|
);
|
||||||
|
return { usageCode, featureCollection: getFeatureCollection(payload) };
|
||||||
|
}));
|
||||||
if (requestSeq !== flowDirectionRequestSeqRef.current) return;
|
if (requestSeq !== flowDirectionRequestSeqRef.current) return;
|
||||||
|
if (abortController.signal.aborted) return;
|
||||||
|
|
||||||
const featureCollection = getFeatureCollection(payload);
|
const responses = settledResponses
|
||||||
renderFlowDirectionLayer(featureCollection, layerInfo, usageStyleMap);
|
.filter((result): result is PromiseFulfilledResult<{ usageCode: string; featureCollection: any }> =>
|
||||||
|
result.status === "fulfilled",
|
||||||
|
)
|
||||||
|
.map((result) => result.value);
|
||||||
|
const failedResponses = settledResponses.filter((result) => result.status === "rejected");
|
||||||
|
|
||||||
|
if (responses.length === 0) {
|
||||||
|
if (!isPartialUsageRequest) {
|
||||||
|
removeFlowDirectionLayer();
|
||||||
|
}
|
||||||
|
throw failedResponses[0]?.reason ?? new Error("유하방향 관종별 조회 결과가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedResponses.length > 0) {
|
||||||
|
console.error("유하방향 일부 관종 조회 실패:", failedResponses.map((result) => result.reason));
|
||||||
|
if (!flowDirectionFailureAlertedRef.current) {
|
||||||
|
flowDirectionFailureAlertedRef.current = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.forEach(({ usageCode, featureCollection }) => {
|
||||||
|
renderFlowDirectionLayer(featureCollection, layerInfo, usageStyleMap);
|
||||||
|
flowDirectionVisibleUsageCodesRef.current.add(usageCode);
|
||||||
|
});
|
||||||
|
const returnedFeaturesByUsage = responses.reduce<Record<string, number>>(
|
||||||
|
(acc, { usageCode, featureCollection }) => {
|
||||||
|
acc[usageCode] = featureCollection.features?.length ?? 0;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const networkCorrectedFeatures = responses.reduce((count, { featureCollection }) => {
|
||||||
|
const correctedCount = featureCollection.features?.filter((feature: any) =>
|
||||||
|
String(
|
||||||
|
feature?.properties?.networkCorrectionYn ??
|
||||||
|
feature?.properties?.network_correction_yn ??
|
||||||
|
"",
|
||||||
|
).toUpperCase() === "Y",
|
||||||
|
).length ?? 0;
|
||||||
|
return count + correctedCount;
|
||||||
|
}, 0);
|
||||||
(window as any).__SN_FLOW_DIRECTION_DEBUG__ = {
|
(window as any).__SN_FLOW_DIRECTION_DEBUG__ = {
|
||||||
screenBbox,
|
screenBbox,
|
||||||
requestBbox,
|
requestBbox,
|
||||||
requestSeq,
|
requestSeq,
|
||||||
zoom,
|
zoom,
|
||||||
|
directionMode: flowDirectionMode,
|
||||||
|
searchStrategy: "BBOX",
|
||||||
usageCodes,
|
usageCodes,
|
||||||
returnedFeatures: featureCollection.features?.length ?? 0,
|
requestUsageCodes,
|
||||||
|
requestCount: responses.length,
|
||||||
|
returnedFeaturesByUsage,
|
||||||
|
returnedFeatures: Object.values(returnedFeaturesByUsage).reduce((sum, count) => sum + count, 0),
|
||||||
|
networkCorrectedFeatures,
|
||||||
cachedFeatures: flowDirectionSourceRef.current?.getFeatures?.().length ?? 0,
|
cachedFeatures: flowDirectionSourceRef.current?.getFeatures?.().length ?? 0,
|
||||||
};
|
};
|
||||||
flowDirectionFailureAlertedRef.current = false;
|
if (failedResponses.length === 0) {
|
||||||
|
flowDirectionFailureAlertedRef.current = false;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestSeq !== flowDirectionRequestSeqRef.current) return;
|
if (requestSeq !== flowDirectionRequestSeqRef.current) return;
|
||||||
if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") return;
|
if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") return;
|
||||||
|
|
||||||
console.error("유하방향 레이어 조회 실패:", error);
|
console.error("유하방향 레이어 조회 실패:", error);
|
||||||
removeFlowDirectionLayer();
|
if (!isPartialUsageRequest) {
|
||||||
|
removeFlowDirectionLayer();
|
||||||
|
}
|
||||||
if (!flowDirectionFailureAlertedRef.current) {
|
if (!flowDirectionFailureAlertedRef.current) {
|
||||||
alert("유하방향 정보를 불러오지 못했습니다.");
|
|
||||||
flowDirectionFailureAlertedRef.current = true;
|
flowDirectionFailureAlertedRef.current = true;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -795,7 +938,7 @@ const Map = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[removeFlowDirectionLayer, renderFlowDirectionLayer],
|
[flowDirectionMode, removeFlowDirectionLayer, renderFlowDirectionLayer, retainFlowDirectionUsageCodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
// SearchModal, GisContent 등에서 호출
|
// SearchModal, GisContent 등에서 호출
|
||||||
@@ -1090,12 +1233,22 @@ const Map = () => {
|
|||||||
setIsFlowDirectionChecked(Boolean(nextFlowDirectionLayerInfo));
|
setIsFlowDirectionChecked(Boolean(nextFlowDirectionLayerInfo));
|
||||||
setFlowDirectionLayerInfo(nextFlowDirectionLayerInfo);
|
setFlowDirectionLayerInfo(nextFlowDirectionLayerInfo);
|
||||||
if (nextFlowDirectionLayerInfo) {
|
if (nextFlowDirectionLayerInfo) {
|
||||||
clearFlowDirectionFeatureCache();
|
const map = gisMaps.current;
|
||||||
|
const zoom = map?.getView?.()?.getZoom?.() ?? 0;
|
||||||
|
const nextUsageStyleMap = extractCheckedPipeUsageStyles(checkedNodesData, zoom);
|
||||||
|
const requestUsageCodes = retainFlowDirectionUsageCodes(Object.keys(nextUsageStyleMap));
|
||||||
|
flowDirectionSelectionRequestUsageCodesRef.current = requestUsageCodes;
|
||||||
|
if (requestUsageCodes.length === 0) {
|
||||||
|
flowDirectionRequestSeqRef.current += 1;
|
||||||
|
flowDirectionAbortControllerRef.current?.abort();
|
||||||
|
flowDirectionAbortControllerRef.current = null;
|
||||||
|
}
|
||||||
setFlowDirectionSelectionVersion((version) => version + 1);
|
setFlowDirectionSelectionVersion((version) => version + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextFlowDirectionLayerInfo) {
|
if (!nextFlowDirectionLayerInfo) {
|
||||||
// 해제 직후 pending API 응답이 늦게 도착해도 renderFlowDirectionLayer가 실행되지 않도록 순번을 무효화한다.
|
// 해제 직후 pending API 응답이 늦게 도착해도 renderFlowDirectionLayer가 실행되지 않도록 순번을 무효화한다.
|
||||||
|
flowDirectionSelectionRequestUsageCodesRef.current = null;
|
||||||
flowDirectionRequestSeqRef.current += 1;
|
flowDirectionRequestSeqRef.current += 1;
|
||||||
flowDirectionAbortControllerRef.current?.abort();
|
flowDirectionAbortControllerRef.current?.abort();
|
||||||
flowDirectionAbortControllerRef.current = null;
|
flowDirectionAbortControllerRef.current = null;
|
||||||
@@ -1105,7 +1258,7 @@ const Map = () => {
|
|||||||
|
|
||||||
handleWaterDepthVisibility(checkedIds, checkedNodesData);
|
handleWaterDepthVisibility(checkedIds, checkedNodesData);
|
||||||
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
|
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
|
||||||
}, [clearFlowDirectionFeatureCache, handleWaterDepthVisibility, removeFlowDirectionLayer]);
|
}, [handleWaterDepthVisibility, removeFlowDirectionLayer, retainFlowDirectionUsageCodes]);
|
||||||
|
|
||||||
const handleOpenLayerPopup = useCallback(
|
const handleOpenLayerPopup = useCallback(
|
||||||
(_layerNm: string, node?: any) => {
|
(_layerNm: string, node?: any) => {
|
||||||
@@ -1194,7 +1347,10 @@ const Map = () => {
|
|||||||
|
|
||||||
// 트리 체크 상태나 관종 레이어 선택이 바뀌면 유하방향 API vector layer를 추가/갱신/제거한다.
|
// 트리 체크 상태나 관종 레이어 선택이 바뀌면 유하방향 API vector layer를 추가/갱신/제거한다.
|
||||||
if (isFlowDirectionChecked) {
|
if (isFlowDirectionChecked) {
|
||||||
loadFlowDirectionLayer(flowDirectionLayerInfo);
|
const requestUsageCodes = flowDirectionSelectionRequestUsageCodesRef.current;
|
||||||
|
flowDirectionSelectionRequestUsageCodesRef.current = null;
|
||||||
|
if (requestUsageCodes && requestUsageCodes.length === 0) return;
|
||||||
|
loadFlowDirectionLayer(flowDirectionLayerInfo, requestUsageCodes ?? undefined);
|
||||||
} else {
|
} else {
|
||||||
removeFlowDirectionLayer();
|
removeFlowDirectionLayer();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../map/page";
|
||||||
Reference in New Issue
Block a user