위성지도 기능 개선 및 침수 레이어 범례 보완

This commit is contained in:
kiminchan
2026-06-18 11:03:39 +09:00
parent 2ea207a63a
commit 2c27a67cc3
3 changed files with 132 additions and 23 deletions
+16 -5
View File
@@ -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 = () => {
침수심 범례
</div>
<div className="space-y-1">
{WATER_DEPTH_LEGEND_ITEMS.map((item, index) => (
<div key={`${item.label}-${index}`} className="flex items-center gap-2">
{waterDepthLegendItems.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<span
className="inline-block h-4 w-10 shrink-0 border border-slate-300"
style={{ backgroundColor: item.color }}
className="inline-block h-4 w-10 shrink-0"
style={{
backgroundColor: item.color,
border: `1px solid ${item.borderColor}`,
}}
/>
<span className="text-[1rem] font-medium text-slate-700">{item.label}</span>
</div>
@@ -4687,6 +4697,7 @@ const Map = () => {
lastSinkholeSyncSummary,
sinkholeRadiusMeters,
userInfo?.adminRoleYn,
waterDepthLegendItems,
]);
// 선택된 설정이 있을 때만 하단 전용 섹션을 노출하고, 세로 공간이 부족하면 내부 스크롤로 처리한다.
@@ -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<string, WaterDepthLegendItem>();
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({
<div className="w-[140px] mx-auto flex flex-col items-center">
<div className="bg-blue-900 text-white text-2xl text-center py-1 mb-2 rounded w-full">침수심 범례</div>
<div className="space-y-1 w-full">
{WATER_DEPTH_LEGEND_ITEMS.map((item, index) => (
{legendItems.map((item) => (
<LegendItem
key={`${item.label}-${index}`}
key={item.key}
color={item.color}
borderColor={item.borderColor}
label={item.label}
/>
))}
@@ -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 (
<div className="flex items-center gap-2 w-full">
<span
className="inline-block w-16 h-6"
style={{ backgroundColor: color, border: "1px solid #ccc" }}
style={{ backgroundColor: color, border: `1px solid ${borderColor}` }}
/>
<span className="text-xl text-gray-900 font-bold">{label}</span>
</div>
@@ -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({