sn: 도시침수지도 geoserver style적용

This commit is contained in:
2026-10-16 18:05:48 +09:00
parent 7911cff96a
commit 79f70d8b4b
4 changed files with 18 additions and 120 deletions
@@ -3211,7 +3211,7 @@ const MapPageClient = () => {
) : null}
{selectedWaterRiskMapView ? (
// Capacity와 Flooding 색상은 SWMM 결과의 업무 의미색이므로 테마와 무관하게 유지한다.
<div className="absolute bottom-4 left-[270px] z-[61] rounded-md border border-border bg-background px-3 py-2 text-[10px] text-foreground shadow-md">
<div className="absolute bottom-4 left-[270px] z-[44] rounded-md border border-border bg-background px-3 py-2 text-[10px] text-foreground shadow-md">
<div className="mb-1 font-bold">Capacity</div>
<div className="space-y-1 font-semibold">
{[
@@ -52,7 +52,7 @@
| 행 선택 지도 이동 | 기존 그룹 범위 이동 유지 |
| 일반 지도 통신 | 빈도 부모 3개 TileWMS |
| 빈도 제한 | 부모 `flterSql`을 WMS `CQL_FILTER`로 전달 |
| 침수심도 스타일 | 자식 5건 기반 동적 SLD |
| 침수심도 스타일 | GeoServer 레이어 기본 스타일(2026-10-16부터, 11절). 이전에는 자식 5건 기반 동적 SLD |
| 범례 | 숨겨진 자식 설정의 기존 색상·명칭 유지 |
| 100년 빈도 | 화면 및 WMS 전환 대상에서 제외 |
| 기존 다른 레이어 | 기존 WFS 경로 유지 |
@@ -88,3 +88,16 @@
- 운영에서 행 선택 후 그룹 extent API가 `ST_Envelope` 스키마 해석 오류로 실패해 지도 이동이 중단된 원인을 백엔드 SQL에서 보완했다.
- 기존 시군구·침수빈도 그룹 범위 이동 계약과 화면 동작은 유지한다.
- 재배포 후 목록 행 클릭, 지도 중심 이동, 선택 빈도 WMS 표출을 회귀 확인한다.
## 11. 2026-10-16 WMS SLD_BODY 제거
- 근거: `COMMON_DOCS_ROOT/작업계획/SN-도시침수지도-정보제공포털-목록보기/003-de-SN-도시침수지도-WMS502-SLD_BODY제거-작업계획.md` (SFR-011-008·009, 화면설계서 24·28쪽 SN_MN010205·SN_MN010206)
- 운영에서 `SLD_BODY`가 포함된 WMS 요청이 nginx 502로 실패해, 침수심도 스타일을 GeoServer 레이어 기본 스타일로 옮기고 요청에서 `SLD_BODY`를 뺐다.
- `gis.layer.js`
- `getWmsLayer()`에서 도시침수지도 `SLD_BODY` 생성 분기와 `gis.wms-sld.js` import를 제거했다.
- `STYLES=''`(레이어 기본 스타일), 빈도 `CQL_FILTER`, `drawOrdr`, `minViewSize`는 그대로다.
- `gis.wms-sld.js`: 삭제했다(사용처가 `gis.layer.js` 하나뿐).
- 범례(`water-depth.tsx`)는 기존처럼 숨겨진 자식 5건의 DB 색상을 쓴다. GeoServer 기본 스타일이 제공 SLD(`tb_ms_flood_dngr_map_depth.sld`)로 교체되어야 지도 색과 범례가 일치한다.
- 검증: 남은 참조 없음, `git diff --check`·ESLint 통과, 개발 GeoServer 동일 파라미터 요청 HTTP 200 PNG.
- 미수행: 사용자 개발 서버 실행 중이라 `business_components`·`sn` 빌드 보류, 로그인 필요로 브라우저 Network 확인 보류.
- 롤백: `gis.layer.js`의 SLD 분기와 `gis.wms-sld.js`를 복원한다.
@@ -21,11 +21,6 @@ import Tile from 'ol/layer/Tile.js';
import TileWMS from 'ol/source/TileWMS.js';
import gisCore from './gis.core.js';
import { showBusinessUserMessage } from '../../../lib/user-message';
import {
buildFloodDngrSldBody,
FLOOD_DNGR_MAP_LAYER_TYPE,
getFloodDngrSldSettings,
} from './gis.wms-sld.js';
/**
* GisApp.layer
@@ -195,19 +190,12 @@ GisOlLayer.prototype = {
params['FORMAT'] = 'image/png';
params['TRANSPARENT'] = true;
params['TILED'] = true;
// 빈 값이면 GeoServer 레이어의 기본 스타일(Default Style)로 그린다.
// 도시침수지도(tb_ms_flood_dngr_map)의 침수심도 색상도 GeoServer에 등록한 기본 스타일을 사용하며,
// 긴 SLD_BODY를 URL에 싣지 않아 운영 프록시의 헤더 크기 초과(502)를 피한다.
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,
@@ -1,103 +0,0 @@
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('&', '&amp;')
.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 };