Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev

# Conflicts:
#	apps/sn/src/features/windowPopup/model/popupDefinitions.ts
#	apps/sn/src/features/windowPopup/registry.tsx
This commit is contained in:
2026-08-05 16:24:52 +09:00
55 changed files with 4173 additions and 145 deletions
@@ -346,7 +346,13 @@ export default React.memo(function GisTree({
// orderedLayers 순서를 우선 적용하면서, 타입 필터에 맞는 타겟 노드만 선별
const getTargetNodesInOrder = () => {
const target = nodes.filter((n) => n.data?.actlLyrYn === 'N' && layerTypes.includes(n.data?.lyrTyNm));
const target = nodes.filter((node) => {
const hasChildren = nodes.some((child) => child.parent === node.id);
const isWmsGroupLayer = node.data?.srvcNm === 'wms' && hasChildren;
return node.data?.exprsYn === 'Y'
&& layerTypes.includes(node.data?.lyrTyNm)
&& (node.data?.actlLyrYn === 'N' || isWmsGroupLayer);
});
// drawOrdr 값을 기준으로 오름차순 정렬
target.sort((a, b) => {
@@ -392,6 +392,7 @@ GisApp.core.prototype = {
*/
addWmsLayer: function (layerCodeObj) {
let layer = GisApp.Module.layer.getWmsLayer(layerCodeObj);
if (!layer) return null;
this.deleteLayer(layer.get('id'));
this.map.addLayer(layer);
return layer;
@@ -20,6 +20,11 @@ import GisOlStyle from './gis.style.js';
import Tile from 'ol/layer/Tile.js';
import TileWMS from 'ol/source/TileWMS.js';
import gisCore from './gis.core.js';
import {
buildFloodDngrSldBody,
FLOOD_DNGR_MAP_LAYER_TYPE,
getFloodDngrSldSettings,
} from './gis.wms-sld.js';
/**
* GisApp.layer
@@ -179,6 +184,7 @@ GisOlLayer.prototype = {
let url = GisApp.GEOSERVER_WMS_URL;
let toProj = GisApp.TO_PROJ;
const lyrTyNm = options.lyrTyNm;
const filter = String(options?.flterSql ?? '').trim();
let params = {};
params['LAYERS'] = this.storageNm + lyrTyNm;
params['SERVICE'] = 'WMS';
@@ -186,7 +192,21 @@ GisOlLayer.prototype = {
params['REQUEST'] = 'GetMap';
params['SRS'] = toProj;
params['FORMAT'] = 'image/png';
params['TRANSPARENT'] = true;
params['TILED'] = true;
params['STYLES'] = '';
if (filter) params['CQL_FILTER'] = filter;
if (lyrTyNm === FLOOD_DNGR_MAP_LAYER_TYPE) {
const sldSettings = getFloodDngrSldSettings(GisApp.layerCode, options?.id);
const sldBody = buildFloodDngrSldBody(lyrTyNm, sldSettings);
if (!sldBody) {
console.warn('[GisApp] 도시침수지도 WMS SLD 생성에 필요한 침수심도 설정 5건이 유효하지 않습니다.', options);
return null;
}
params['SLD_BODY'] = sldBody;
}
const wmsLayer = new Tile({
...layerOptions,
id: layerOptions?.id,
@@ -196,8 +216,9 @@ GisOlLayer.prototype = {
serverType: 'geoserver',
}),
});
wmsLayer.setZIndex(options.draw0rdr);
wmsLayer.setMinZoom(options.minViewSize);
wmsLayer.set('id', String(layerOptions?.id));
wmsLayer.setZIndex(Number(options?.drawOrdr) || 1);
wmsLayer.setMinZoom(Number(options?.minViewSize) || 0);
this.layers.push(wmsLayer);
return wmsLayer;
},
@@ -29,9 +29,11 @@ GisOlSelect.prototype = {
dragBox: null,
dragPolygon: null,
selectedFeatures: [],
focusedBoundaryFeatures: null,
selectFeatureFlag: false,
initialize: function (map, options) {
this.map = map;
this.focusedBoundaryFeatures = new WeakSet();
Object.assign(this.defaults, options);
this.setInteraction();
this.pointerMoveEventHandler();
@@ -107,7 +109,8 @@ GisOlSelect.prototype = {
}
}
const geometryType = feature?.getGeometry?.()?.getType?.();
if (geometryType === 'LineString' || geometryType === 'MultiLineString') {
const focusedBoundary = me.focusedBoundaryFeatures.has(feature);
if (focusedBoundary || geometryType === 'LineString' || geometryType === 'MultiLineString') {
styles.push(...focusedLineStyle);
} else {
styles.push(baseStyle);
@@ -329,6 +332,7 @@ GisOlSelect.prototype = {
selectedFeatures.remove(feature);
});
selectedFeatures.clear();
this.focusedBoundaryFeatures = new WeakSet();
},
/**
* SELECT 이벤트 활성화
@@ -491,6 +495,20 @@ GisOlSelect.prototype = {
auto: auto,
});
},
/**
* 폴리곤을 선형 피처와 동일한 다중 경계 스타일로 강조한다.
*/
selectFocusedBoundaryFeature: function (feature, auto) {
let me = this;
me.focusedBoundaryFeatures.add(feature);
me.selectedFeatures.push(feature);
me.selectInteraction.dispatchEvent({
type: 'select',
selected: me.selectedFeatures,
deselected: [],
auto: auto,
});
},
};
export default GisOlSelect;
@@ -0,0 +1,103 @@
const FLOOD_DNGR_MAP_LAYER_TYPE = 'tb_ms_flood_dngr_map';
const FLOOD_DEPTH_ATTRIBUTE = 'sbmrg_dpthl_cd';
const FLOOD_DEPTH_CODES = ['N330', 'N331', 'N332', 'N333', 'N334'];
const escapeXml = (value) => String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
const toHex = (value) => Math.round(value).toString(16).padStart(2, '0');
const parseCssColor = (value) => {
const normalized = String(value ?? '').trim();
const rgbaMatch = normalized.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(0(?:\.\d+)?|1(?:\.0+)?))?\s*\)$/i);
if (rgbaMatch) {
const red = Number(rgbaMatch[1]);
const green = Number(rgbaMatch[2]);
const blue = Number(rgbaMatch[3]);
const opacity = rgbaMatch[4] == null ? 1 : Number(rgbaMatch[4]);
if ([red, green, blue].some((channel) => channel < 0 || channel > 255) || opacity < 0 || opacity > 1) {
return null;
}
return {
hex: `#${toHex(red)}${toHex(green)}${toHex(blue)}`,
opacity,
};
}
const hexMatch = normalized.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
if (!hexMatch) return null;
const rawHex = hexMatch[1].length === 3
? hexMatch[1].split('').map((character) => character + character).join('')
: hexMatch[1];
return {
hex: `#${rawHex.toLowerCase()}`,
opacity: 1,
};
};
const getFloodDepthCode = (filterSql) => {
const match = String(filterSql ?? '').match(/\bsbmrg_dpthl_cd\s*=\s*'([^']+)'/i);
const depthCode = match?.[1]?.trim();
return FLOOD_DEPTH_CODES.includes(depthCode) ? depthCode : null;
};
const getLineWidth = (value) => {
const lineWidth = Number(value);
return Number.isFinite(lineWidth) && lineWidth >= 0 && lineWidth <= 20 ? lineWidth : null;
};
/**
* 빈도 부모 레이어 바로 아래에 있는 침수심도 설정만 선별한다.
* 부모는 WMS 요청을 담당하고, 반환된 자식 5건은 SLD 규칙의 필터와 색상 원본으로 사용한다.
*/
export const getFloodDngrSldSettings = (layerCode, parentLayerId) => Object.values(layerCode ?? {})
.filter((layerSetting) => (
String(layerSetting?.upperLyrSn ?? '') === String(parentLayerId ?? '')
&& layerSetting?.lyrTyNm === FLOOD_DNGR_MAP_LAYER_TYPE
))
.sort((left, right) => Number(left?.sortOrdr ?? 0) - Number(right?.sortOrdr ?? 0));
/**
* DB의 침수심도 필터와 RGBA 스타일을 GeoServer가 해석할 수 있는 SLD 1.0 XML로 변환한다.
* 다섯 침수심도 코드나 색상값 중 하나라도 유효하지 않으면 잘못된 기본 스타일 표출을 막기 위해 null을 반환한다.
*/
export const buildFloodDngrSldBody = (layerName, layerSettings) => {
if (!Array.isArray(layerSettings) || layerSettings.length !== FLOOD_DEPTH_CODES.length) return null;
const settingsByDepthCode = new Map();
for (const layerSetting of layerSettings) {
const depthCode = getFloodDepthCode(layerSetting?.flterSql);
if (depthCode && !settingsByDepthCode.has(depthCode)) {
settingsByDepthCode.set(depthCode, layerSetting);
}
}
if (settingsByDepthCode.size !== FLOOD_DEPTH_CODES.length) return null;
const rules = [];
for (const depthCode of FLOOD_DEPTH_CODES) {
const layerSetting = settingsByDepthCode.get(depthCode);
const fillColor = parseCssColor(layerSetting?.polgnColor);
const strokeColor = parseCssColor(layerSetting?.lnColor);
const lineWidth = getLineWidth(layerSetting?.lnWdth);
if (!fillColor || !strokeColor || lineWidth == null) return null;
rules.push(
`<sld:Rule><sld:Name>${escapeXml(depthCode)}</sld:Name><sld:Title>${escapeXml(layerSetting?.lyrNm || depthCode)}</sld:Title>`
+ `<ogc:Filter><ogc:PropertyIsEqualTo><ogc:PropertyName>${FLOOD_DEPTH_ATTRIBUTE}</ogc:PropertyName><ogc:Literal>${escapeXml(depthCode)}</ogc:Literal></ogc:PropertyIsEqualTo></ogc:Filter>`
+ `<sld:PolygonSymbolizer><sld:Fill><sld:CssParameter name="fill">${fillColor.hex}</sld:CssParameter><sld:CssParameter name="fill-opacity">${fillColor.opacity}</sld:CssParameter></sld:Fill>`
+ `<sld:Stroke><sld:CssParameter name="stroke">${strokeColor.hex}</sld:CssParameter><sld:CssParameter name="stroke-opacity">${strokeColor.opacity}</sld:CssParameter><sld:CssParameter name="stroke-width">${lineWidth}</sld:CssParameter></sld:Stroke></sld:PolygonSymbolizer></sld:Rule>`
);
}
return `<?xml version="1.0" encoding="UTF-8"?>`
+ `<sld:StyledLayerDescriptor version="1.0.0" xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc">`
+ `<sld:NamedLayer><sld:Name>${escapeXml(layerName)}</sld:Name><sld:UserStyle><sld:FeatureTypeStyle>${rules.join('')}</sld:FeatureTypeStyle></sld:UserStyle></sld:NamedLayer>`
+ `</sld:StyledLayerDescriptor>`;
};
export { FLOOD_DNGR_MAP_LAYER_TYPE };
@@ -269,6 +269,19 @@ export const ProcSearchBar = ({
[sigunData],
);
// 외부 지도 진입 이벤트가 시군구 원천 조회보다 먼저 도착해도,
// 원천 목록이 준비되는 즉시 현재 시도에 맞는 선택 후보를 다시 구성한다.
useEffect(() => {
if (sidoValue === "0") {
setFilteredSigunData([{ text: "시군구", val: "0" }]);
return;
}
const prefix = sidoValue.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]);
}, [sidoValue, sigunData]);
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {