From 2c27a67cc36588003b999540188aca19ad8941d5 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Thu, 18 Jun 2026 11:03:39 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9C=84=EC=84=B1=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=B9=A8?= =?UTF-8?q?=EC=88=98=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=B2=94=EB=A1=80=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/map/page.tsx | 21 +++- .../map/waterDepth/waterDepth.tsx | 113 ++++++++++++++++-- .../src/ui/map/lib/gis.core.js | 21 ++-- 3 files changed, 132 insertions(+), 23 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 95f1cbb8a..dfb64a1ee 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -121,7 +121,7 @@ import { } from "./components/floodHistory/floodHistoryTypes"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; import SearchModal from "./searchModal/searchModal"; -import { WATER_DEPTH_LAYER_TYPES, WATER_DEPTH_LEGEND_ITEMS } from "./waterDepth/waterDepth"; +import { WATER_DEPTH_LAYER_TYPES, extractWaterDepthLegendItems } from "./waterDepth/waterDepth"; import { buildWaterRiskStatusLabel, EMPTY_WATER_RISK_SESSION_SNAPSHOT, @@ -2289,6 +2289,7 @@ const Map = () => { const [isWaterDepthLayerActive, setIsWaterDepthLayerActive] = useState(false); // 침수심 레이어 활성 여부 const [isPlaying, setIsPlaying] = useState(false); const [mapReady, setMapReady] = useState(false); + const [checkedLayerLegendVersion, setCheckedLayerLegendVersion] = useState(0); const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false); const [isSinkholeChecked, setIsSinkholeChecked] = useState(false); const [isFloodHistoryChecked, setIsFloodHistoryChecked] = useState(false); @@ -4524,6 +4525,7 @@ const Map = () => { // 체크된 레이어 변경 처리 함수 const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { checkedNodesDataRef.current = checkedNodesData; + setCheckedLayerLegendVersion((version) => version + 1); // 유하방향은 일반 setLayerOnMap 대상이 아니므로 체크 노드 여부를 별도로 추적한다. const flowDirectionNode = checkedNodesData.find((node) => @@ -4576,6 +4578,11 @@ const Map = () => { } }, [clearFloodHistorySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]); + const waterDepthLegendItems = useMemo( + () => extractWaterDepthLegendItems(checkedNodesDataRef.current ?? []), + [checkedLayerLegendVersion], + ); + const handleOpenLayerPopup = useCallback( (_layerNm: string, node?: any) => { if (isAgedPipeLayerNode(node)) { @@ -4657,11 +4664,14 @@ const Map = () => { 침수심 범례
- {WATER_DEPTH_LEGEND_ITEMS.map((item, index) => ( -
+ {waterDepthLegendItems.map((item) => ( +
{item.label}
@@ -4687,6 +4697,7 @@ const Map = () => { lastSinkholeSyncSummary, sinkholeRadiusMeters, userInfo?.adminRoleYn, + waterDepthLegendItems, ]); // 선택된 설정이 있을 때만 하단 전용 섹션을 노출하고, 세로 공간이 부족하면 내부 스크롤로 처리한다. diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index e1a1fcb53..8b03a82e8 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -20,13 +20,101 @@ export const WATER_DEPTH_LAYER_TYPES: string[] = [ 'tb_ms_sbmrg_dngr_map' // 침수위험지도 ]; -export const WATER_DEPTH_LEGEND_ITEMS = [ - { color: "rgb(255, 255, 127)", label: "0.0~0.5m 미만" }, - { color: "rgb(191, 255, 0)", label: "0.5~1.0m 미만" }, - { color: "rgb(0, 255, 255)", label: "1.0~2.0m 미만" }, - { color: "rgb(191, 127, 255)", label: "2.0~5.0m 미만" }, - { color: "rgb(255, 0, 127)", label: "5.0m 이상" }, -] as const; +export type WaterDepthLegendItem = { + key: string; + label: string; + color: string; + borderColor: string; + sortSeq: number; +}; + +const getLayerTypeName = (layerInfo: any) => + String(layerInfo?.lyrTyNm ?? layerInfo?.lyrTypeNm ?? layerInfo?.lyr_type_nm ?? "").trim(); + +const getFiniteSortOrder = (value: unknown) => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER; +}; + +const getLegendNumericOrder = (label: string) => { + const normalized = String(label).trim(); + const rangeMatch = normalized.match(/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)m/); + if (rangeMatch) { + return { + start: Number(rangeMatch[1]), + end: Number(rangeMatch[2]), + priority: 0, + }; + } + + const overMatch = normalized.match(/^(\d+(?:\.\d+)?)m\s*이상/); + if (overMatch) { + const value = Number(overMatch[1]); + return { + start: value, + end: value, + priority: 1, + }; + } + + return { + start: Number.MAX_SAFE_INTEGER, + end: Number.MAX_SAFE_INTEGER, + priority: 2, + }; +}; + +const getLegendColor = (layerInfo: any) => + String(layerInfo?.polgnColor ?? layerInfo?.spacColor ?? layerInfo?.lnColor ?? layerInfo?.lineColor ?? "").trim(); + +const getLegendBorderColor = (layerInfo: any) => + String(layerInfo?.lnColor ?? layerInfo?.lineColor ?? layerInfo?.polgnColor ?? layerInfo?.spacColor ?? "").trim(); + +const hasLegendStyle = (layerInfo: any) => getLegendColor(layerInfo) !== "" || getLegendBorderColor(layerInfo) !== ""; + +const normalizeLegendLabel = (label: string) => { + const normalized = String(label).trim(); + return normalized.endsWith("미만") ? normalized : `${normalized} 미만`; +}; + +export const extractWaterDepthLegendItems = (checkedNodesData: any[]): WaterDepthLegendItem[] => { + if (!Array.isArray(checkedNodesData) || checkedNodesData.length === 0) return []; + + const items = new Map(); + + checkedNodesData.forEach((node) => { + const layerInfo = node?.data ?? {}; + const layerTypeName = getLayerTypeName(layerInfo); + if (!WATER_DEPTH_LAYER_TYPES.includes(layerTypeName)) return; + if (!hasLegendStyle(layerInfo)) return; + + const itemKey = String(node?.text ?? layerInfo?.lyrNm ?? "").trim(); + const itemColor = getLegendColor(layerInfo); + const itemBorderColor = getLegendBorderColor(layerInfo); + + if (!itemKey || !itemColor) return; + if (!items.has(itemKey)) { + items.set(itemKey, { + key: itemKey, + label: normalizeLegendLabel(itemKey), + color: itemColor, + borderColor: itemBorderColor || itemColor, + sortSeq: getFiniteSortOrder(layerInfo?.sortSeq), + }); + } + }); + + return Array.from(items.values()).sort((left, right) => { + const leftOrder = getLegendNumericOrder(left.key); + const rightOrder = getLegendNumericOrder(right.key); + + if (leftOrder.start !== rightOrder.start) return leftOrder.start - rightOrder.start; + if (leftOrder.priority !== rightOrder.priority) return leftOrder.priority - rightOrder.priority; + if (leftOrder.end !== rightOrder.end) return leftOrder.end - rightOrder.end; + if (left.sortSeq !== right.sortSeq) return left.sortSeq - right.sortSeq; + return left.label.localeCompare(right.label, "ko"); + }); +}; interface WaterDepthProps { onClose?: () => void; @@ -34,6 +122,7 @@ interface WaterDepthProps { setIsPlaying: (isPlaying: boolean) => void; totalSteps?: number; currentStep?: number; + legendItems?: WaterDepthLegendItem[]; } export function WaterDepth({ @@ -42,6 +131,7 @@ export function WaterDepth({ setIsPlaying, totalSteps = 0, currentStep = 0, + legendItems = [], }: WaterDepthProps) { useEffect(() => { @@ -80,10 +170,11 @@ export function WaterDepth({
침수심 범례
- {WATER_DEPTH_LEGEND_ITEMS.map((item, index) => ( + {legendItems.map((item) => ( ))} @@ -95,12 +186,12 @@ export function WaterDepth({ ); } -function LegendItem({ color, label }: { color: string; label: string }) { +function LegendItem({ color, borderColor, label }: { color: string; borderColor: string; label: string }) { return (
{label}
diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index b136cdcee..e9e652183 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -45,7 +45,7 @@ GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${G // NGII 정보(배경지도) //GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL; -GisApp.NGII_WMTS_URL = `/ngii-server/openapi/Gettile.do?apikey=${process.env.NEXT_PUBLIC_NGII_PARAM}`; +GisApp.NGII_WMTS_PARAM = process.env.NEXT_PUBLIC_NGII_PARAM ?? ''; GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; @@ -79,6 +79,17 @@ GisApp.setLayerInfos = function (layerInfos) { GisApp.layerInfos = layerInfos; }; +GisApp.getNgiiWmtsUrl = function (layerName, matrix, x, y) { + const layerAwareParams = /(^|&)layer=[^&]*/i.test(GisApp.NGII_WMTS_PARAM) + ? GisApp.NGII_WMTS_PARAM.replace(/(^|&)layer=[^&]*/i, `$1layer=${layerName}`) + : `${GisApp.NGII_WMTS_PARAM}&layer=${layerName}`; + + return `/ngii-server/openapi/Gettile.do?apikey=${layerAwareParams}` + .replace("{z}", matrix) + .replace("{x}", x) + .replace("{y}", y); +}; + /** * GisApp core * @requires gis.layer.js @@ -161,7 +172,7 @@ GisApp.core.prototype = { }); const wmtsSource = new TileWMTS({ - //url: GisApp.NGII_WMTS_URL, + // url은 setTileUrlFunction에서 layer별로 동적으로 만든다. projection: projection, tileGrid: tileGrid, crossOrigin: "anonymous", @@ -175,11 +186,7 @@ GisApp.core.prototype = { wmtsSource.setTileUrlFunction((tileCoord) => { const [z, x, y] = tileCoord; const matrix = matrixIds[z]; - // console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y)); - return GisApp.NGII_WMTS_URL - .replace("{z}", matrix) - .replace("{x}", x) - .replace("{y}", y); + return GisApp.getNgiiWmtsUrl(_layer, matrix, x, y); }); return new TileLayer({