sn: 침수예측지도 오버레이 & 침수흔적깊이

This commit is contained in:
2026-08-27 22:33:37 +09:00
parent 2c3961644c
commit c1ffc33834
10 changed files with 185 additions and 66 deletions
@@ -519,6 +519,15 @@ export const FldngWtlvMapCol2 = [
{ id: 'rainTot', title: '총강우량(mm)' },
{ id: 'rainStr', title: '강우강도(mm/hr)' },
];
export const FldngDpthMapCol = [
{ id: 'fldng_yr', title: '침수년도' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'fldng_dpth', title: '침수깊이' },
{ id: 'fldng_area', title: '침수면적' },
{ id: 'shape_area', title: '형상면적' },
{ id: 'shape_len', title: '형상길이' },
];
export const FldngWtlvMapExcelCol = {
'fldngYr': '침수년도',
'fldngDstrNm': '침수재해명',
@@ -82,8 +82,14 @@ const resolveRegionNames = (
sggOptions: Array<{ text: string; val: string | number }>,
): FldngHstryTableRow => ({
...row,
ctpvNm: resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) || row.ctpvNm,
sggNm: resolveOptionText(row.sggCd ?? "", sggOptions, 5) || row.sggNm,
ctpvNm:
resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) ||
row.ctpvNm ||
String((row as any).ctpv_nm ?? row.ctpvCd ?? "").trim(),
sggNm:
resolveOptionText(row.sggCd ?? "", sggOptions, 5) ||
row.sggNm ||
String((row as any).sgg_nm ?? row.sggCd ?? "").trim(),
});
export default function FldngHstryPopupPage() {
@@ -147,8 +153,14 @@ export default function FldngHstryPopupPage() {
const hydrated = attachFldngHstryGeometry(item);
return {
...hydrated,
ctpvNm: resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2),
sggNm: resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5),
ctpvNm:
resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2) ||
String((hydrated as any).ctpvNm ?? (hydrated as any).ctpv_nm ?? "").trim() ||
String(hydrated.ctpvCd ?? "").trim(),
sggNm:
resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5) ||
String((hydrated as any).sggNm ?? (hydrated as any).sgg_nm ?? "").trim() ||
String(hydrated.sggCd ?? "").trim(),
};
});
@@ -6,7 +6,7 @@ export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup";
export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT";
export const FLOOD_HISTORY_LAYER_TYPE = "tb_ms_fldng_wtlv_map";
export const FLOOD_HISTORY_PREVIEW_LAYER_TYPE = "tb_sn_flood_trace_mock";
export const FLOOD_HISTORY_LAYER_NAME = "침수흔적도";
export const FLOOD_HISTORY_LAYER_NAME = "침수흔적수위";
export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
export const FLDNG_HSTRY_PAGE_SIZE = 10;
@@ -119,13 +119,13 @@ export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
] as const;
export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [
{ id: "rowNo", title: "순번", widthPercent: 5 },
{ id: "floodYearDisplay", title: "침수\n년도", widthPercent: 6 },
{ id: "rowNo", title: "순번", widthPercent: 3 },
{ id: "floodYearDisplay", title: "침수\n년도", widthPercent: 4 },
{ id: "ctpvNm", title: "시도", widthPercent: 7 },
{ id: "sggNm", title: "시군구", widthPercent: 8 },
{ id: "disasterName", title: "침수재해명", widthPercent: 14 },
{ id: "rainBgngTmDisplay", title: "강우\n시작일시", widthPercent: 10 },
{ id: "rainEndTmDisplay", title: "강우\n종료일시", widthPercent: 10 },
{ id: "disasterName", title: "침수재해명", widthPercent: 20 },
{ id: "rainBgngTmDisplay", title: "강우\n시작일시", widthPercent: 9 },
{ id: "rainEndTmDisplay", title: "강우\n종료일시", widthPercent: 9 },
{ id: "rainTotDisplay", title: "총강우량(mm)", widthPercent: 8 },
{ id: "rainStrDisplay", title: "강우강도(mm/hr)", widthPercent: 8 },
{ id: "floodHourDisplay", title: "침수시간", widthPercent: 7 },
@@ -35,6 +35,7 @@ import { getLayerContentComponent } from "../common";
import {
FldngWtlvMapCol,
FldngWtlvMapCol2,
FldngDpthMapCol,
SwtrmtFcltCol,
SwtrmtFcltCol2,
SwtrmtPrcsZoneCol,
@@ -178,6 +179,9 @@ const FeatureClickEventPopup = ({
const linkedFacilityFeatureRef = useRef<any>(null);
const linkedFacilityOriginalStyleRef = useRef<any>(null);
const [mngInstOptions, setMngInstOptions] = useState<MngInstOption[]>([]);
const [floodDepthRows, setFloodDepthRows] = useState<Record<string, unknown>[]>([]);
const [selectedFloodDepthYear, setSelectedFloodDepthYear] = useState<string | null>(null);
const floodDepthYearListRef = useRef<HTMLDivElement>(null);
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
// 🎯 관로관리번호 추출
@@ -466,7 +470,7 @@ const FeatureClickEventPopup = ({
break;
}
case "tb_ms_fldng_wtlv_map": {
result = "침수흔적도";
result = "침수흔적수위";
setHeadlist(FldngWtlvMapCol);
setLayerSpecificNm(
normalizeFldngWtlvMapDisplayId(
@@ -475,6 +479,12 @@ const FeatureClickEventPopup = ({
);
break;
}
case "tb_ms_fldng_dpth_map": {
result = "침수흔적깊이";
setHeadlist(FldngDpthMapCol);
setLayerSpecificNm(String(featureData.values_?.data_sn ?? featureData.data_sn ?? featureId ?? ""));
break;
}
case "tb_ms_stofcbr": {
result = "우수토실";
setHeadlist(stofcbrCol);
@@ -539,11 +549,17 @@ const FeatureClickEventPopup = ({
break;
}
case "tb_ms_fldng_wtlv_map": {
result = "침수흔적도";
result = "침수흔적수위";
setHeadlist(FldngWtlvMapCol2);
setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.fid));
break;
}
case "tb_ms_fldng_dpth_map": {
result = "침수흔적깊이";
setHeadlist(FldngDpthMapCol);
setLayerSpecificNm(String(featureData.dataSn ?? featureData.data_sn ?? featureData.fid ?? ""));
break;
}
case "tb_ms_stofcbr": {
result = "우수토실";
setHeadlist(stofcbrCol2);
@@ -596,13 +612,14 @@ const FeatureClickEventPopup = ({
// 🎯 기본 데이터 포맷팅
const normalizedRegionData =
selectedLayerName === "침수흔적도"
(selectedLayerName === "침수흔적수위" ||
selectedLayerName === "침수흔적깊이")
? withResolvedRegionNames(data)
: data;
const formattedData = withMngInstName(
formatDisplayData(normalizedRegionData),
);
if (selectedLayerName === "침수흔적도") {
if (selectedLayerName === "침수흔적수위") {
const fldngBgngDtm = formatFldngWtlvDateTime(
formattedData.bgngYmd ??
formattedData.bgng_ymd ??
@@ -684,7 +701,7 @@ const FeatureClickEventPopup = ({
case "하수처리구역":
setButtonChk(false);
break;
case "침수흔적도":
case "침수흔적수위":
setButtonChk(false);
break;
case "우수토실":
@@ -759,7 +776,7 @@ const FeatureClickEventPopup = ({
}
}
if (selectedLayerName === "침수흔적도" && featureId) {
if (selectedLayerName === "침수흔적수위" && featureId) {
try {
const normalizedFloodMapFid =
normalizeFldngWtlvMapApiFid(featureId);
@@ -777,6 +794,25 @@ const FeatureClickEventPopup = ({
setFetchData(mergedData);
setTableData(mergedData);
}
const depthDataSn = Number(
data?.data_sn ?? data?.dataSn ?? formattedData?.data_sn ?? formattedData?.dataSn,
);
if (Number.isFinite(depthDataSn)) {
const depthRows = await getData(
`v1/fldng/hstry/depth-map/${encodeURIComponent(String(depthDataSn))}`,
);
const normalizedDepthRows = Array.isArray(depthRows)
? depthRows.filter((row) => row && typeof row === "object")
: [];
setFloodDepthRows(normalizedDepthRows);
const years = normalizedDepthRows
.map((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim())
.filter(Boolean);
setSelectedFloodDepthYear(years[0] ?? null);
} else {
setFloodDepthRows([]);
setSelectedFloodDepthYear(null);
}
} catch (rainError) {
console.warn("침수강우분석 조회 실패:", rainError);
}
@@ -1009,6 +1045,48 @@ const FeatureClickEventPopup = ({
total2={0}
isEditable2={buttonChk}
/>
{layerNm === "침수흔적수위" && floodDepthRows.length > 0 && (
<div className="mx-4 mb-4 rounded-md border border-border bg-background p-3">
<div className="mb-2 flex items-center gap-1">
<button type="button" aria-label="이전 연도" className="shrink-0 px-1 text-blue-600" onClick={() => floodDepthYearListRef.current?.scrollBy({ left: -120, behavior: "smooth" })}>‹</button>
<div ref={floodDepthYearListRef} className="flex min-w-0 flex-1 gap-2 overflow-x-auto whitespace-nowrap">
{Array.from(
new Set(
floodDepthRows
.map((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim())
.filter(Boolean),
),
).map((year) => (
<button
key={year}
type="button"
onClick={() => setSelectedFloodDepthYear(year)}
className={`rounded-full border px-3 py-1 text-sm ${selectedFloodDepthYear === year ? "border-blue-600 bg-blue-600 text-white" : "border-blue-400 bg-transparent text-blue-600"}`}
>
{year}
</button>
))}
</div>
<button type="button" aria-label="다음 연도" className="shrink-0 px-1 text-blue-600" onClick={() => floodDepthYearListRef.current?.scrollBy({ left: 120, behavior: "smooth" })}>›</button>
</div>
<div className="max-h-64 overflow-auto">
<table className="w-full border-collapse text-sm">
<tbody>
{Object.entries(
floodDepthRows.find((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim() === selectedFloodDepthYear) ?? {},
)
.filter(([key]) => !key.toLowerCase().includes("geom"))
.map(([key, value]) => (
<tr key={key}>
<th className="border border-border bg-secondary px-2 py-1 text-left">{key}</th>
<td className="border border-border px-2 py-1">{String(value ?? "-")}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{ContentComponent && (
<ContentComponent
featureData={fetchData}
+34 -44
View File
@@ -33,6 +33,7 @@ import { usePathname, useSearchParams } from "next/navigation";
import Collection from "ol/Collection.js";
import Feature from "ol/Feature.js";
import GeoJSON from "ol/format/GeoJSON.js";
import Overlay from "ol/Overlay.js";
import CircleGeom from "ol/geom/Circle.js";
import Draw from "ol/interaction/Draw.js";
import Snap from "ol/interaction/Snap.js";
@@ -145,6 +146,7 @@ import {
FLOOD_HISTORY_DEPTH_LAYER_ID,
FLOOD_HISTORY_PREVIEW_LAYER_TYPE,
FLOOD_HISTORY_LAYER_TYPE,
FLOOD_HISTORY_LAYER_NAME,
FLOOD_HISTORY_POPUP_KEY,
FLDNG_HSTRY_ROW_SELECT_MESSAGE,
isFldngHstryNode,
@@ -238,23 +240,6 @@ const createExmnManholeStyle = () =>
});
/** 선택한 도시침수지도 중앙에 침수예측 분석값을 표시하는 오버레이 스타일이다. */
const createFloodPredictionOverlayStyle = (feature: any) => {
const risk = String(feature?.get?.("floodRiskGrade") ?? "-");
const strokeColor = risk === "심각" ? "#dc2626" : risk === "경계" ? "#f97316" : "#2563eb";
return new Style({
image: new CircleStyle({ radius: 8, fill: new Fill({ color: strokeColor }), stroke: new Stroke({ color: "#ffffff", width: 2 }) }),
text: new Text({
text: String(feature?.get?.("floodPredictionLabel") ?? ""),
font: "bold 12px sans-serif",
textAlign: "center",
textBaseline: "middle",
fill: new Fill({ color: "#111827" }),
stroke: new Stroke({ color: "#ffffff", width: 4 }),
overflow: true,
}),
});
};
// 유하방향은 GeoServer WMS/WFS 레이어가 아니라 BE 보정 GeoJSON API로 그리는 가상 레이어이다.
const FLOW_DIRECTION_LAYER_TYPE = "tb_sn_swage_ppl_flow_direction";
const FLOW_DIRECTION_LAYER_NAME = "유하방향";
@@ -1092,7 +1077,9 @@ const getRightPanelFeatureTitle = (data: any) => {
case "tb_ms_swage_prcs_zone":
return "처리구역";
case "tb_ms_fldng_wtlv_map":
return "침수흔적도";
return FLOOD_HISTORY_LAYER_NAME;
case "tb_ms_fldng_dpth_map":
return "침수흔적깊이";
case "om_dt_untrt_swg_rain":
return "중점우수";
default:
@@ -3237,8 +3224,7 @@ const Map = () => {
const waterRiskManholeLayerRef = useRef<VectorLayer<any> | null>(null);
const waterRiskManholeSourceRef = useRef<VectorSource | null>(null);
// 침수예측현황 선택행에 따라 초단기예보 격자 중앙 라벨을 교체하는 전용 overlay를 보관한다.
const floodPredictionGridLayerRef = useRef<VectorLayer<any> | null>(null);
const floodPredictionGridSourceRef = useRef<VectorSource | null>(null);
const floodPredictionGridLayerRef = useRef<Overlay | null>(null);
const gisEditDrawRef = useRef<any | null>(null);
const gisEditDrawSnapRef = useRef<any | null>(null);
const gisEditTranslateRef = useRef<any | null>(null);
@@ -3517,8 +3503,11 @@ const Map = () => {
const removeFloodPredictionGridLayer = useCallback(() => {
const core = GisApp.Module.core as any;
core?.deleteLayer?.(FLOOD_PREDICTION_GRID_LAYER_ID);
const map = gisMaps.current;
if (map && floodPredictionGridLayerRef.current) {
map.removeOverlay(floodPredictionGridLayerRef.current);
}
floodPredictionGridLayerRef.current = null;
floodPredictionGridSourceRef.current = null;
}, []);
/** 선택행의 시군구·분석시각으로 도시침수지도 중앙 오버레이를 표시한다. */
@@ -3534,28 +3523,29 @@ const Map = () => {
const payload = response?.data ?? response;
const row = payload && !Array.isArray(payload) ? payload : null;
if (!row || extent.length !== 4 || extent.some((value) => !Number.isFinite(value))) return;
const source = new VectorSource();
const center = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
const feature = new Feature({ geometry: new Point(center) });
const observed = Number(row.observedAccumulatedRainfall ?? 0).toFixed(1);
const predicted = Number(row.predictedAccumulatedRainfall ?? 0).toFixed(1);
feature.set("floodRiskGrade", row.floodRiskGrade ?? "-");
feature.set("floodPredictionLabel", `${row.ctpvNm ?? row.ctpvCd ?? "-"} ${row.sggNm ?? row.sggCd ?? "-"}\n분석시각 ${formatFloodPredictionDateTime(row.analysisDateTime ?? analysisDateTime)}\n강우시작시각 ${formatFloodPredictionDateTime(row.rainBeginningDateTime)}\n관측 누적강수량 ${observed}mm\n예측시각 ${formatFloodPredictionDateTime(row.predictionDateTime)}\n예측 누적강수량 ${predicted}mm\n초과 빈도 ${row.exceededFrequencyYear ?? "-"}년\n침수위험도 ${row.floodRiskGrade ?? "-"}`);
source.addFeature(feature);
const layer = new VectorLayer({
source,
zIndex: 9997,
style: createFloodPredictionOverlayStyle,
updateWhileAnimating: false,
updateWhileInteracting: false,
});
layer.set("id", FLOOD_PREDICTION_GRID_LAYER_ID);
layer.set("lyrNm", "침수예측지도 중앙 오버레이");
layer.set("lyrTyNm", "SN_FLDNG_PREDICTION_OVERLAY");
layer.set("clickPsblYn", "N");
map.addLayer(layer);
floodPredictionGridLayerRef.current = layer;
floodPredictionGridSourceRef.current = source;
const container = document.createElement("div");
container.style.cssText = "position:relative; background:rgba(255,255,255,.55); color:#111827; border:1px solid rgba(17,24,39,.7); box-shadow:0 2px 8px rgba(0,0,0,.2); padding:8px 10px; font:400 11px/1.35 sans-serif; min-width:230px;";
const close = document.createElement("button");
close.type = "button";
close.textContent = "×";
close.title = "침수예측 오버레이 닫기";
close.style.cssText = "position:absolute; right:3px; top:1px; border:0; background:transparent; color:#111827; font-size:16px; line-height:16px; cursor:pointer;";
close.addEventListener("click", () => removeFloodPredictionGridLayer());
const table = document.createElement("table");
table.style.cssText = "border-collapse:collapse; width:100%;";
const title = table.createTHead().insertRow();
const titleCell = title.insertCell(); titleCell.colSpan = 2; titleCell.textContent = `${row.ctpvNm ?? row.ctpvCd ?? "-"} ${row.sggNm ?? row.sggCd ?? "-"} 침수예측`; titleCell.style.cssText = "font-weight:500; text-align:center; padding:2px 16px 6px 2px; border-bottom:1px solid #9ca3af;";
const body = table.createTBody();
[["분석시각", formatFloodPredictionDateTime(row.analysisDateTime ?? analysisDateTime)], ["강우시작시각", formatFloodPredictionDateTime(row.rainBeginningDateTime)], ["관측 누적강수량(mm)", observed], ["예측시각", formatFloodPredictionDateTime(row.predictionDateTime)], ["예측 누적강수량(mm)", predicted], ["초과 빈도", `${row.exceededFrequencyYear ?? "-"}년`], ["침수위험도", row.floodRiskGrade ?? "-"]].forEach(([label, value]) => { const tr = body.insertRow(); const key = tr.insertCell(); key.textContent = label; key.style.cssText = "font-weight:400; padding:2px 8px 2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; const val = tr.insertCell(); val.textContent = value; val.style.cssText = "font-weight:400; text-align:right; padding:2px 2px; white-space:nowrap; border-bottom:1px solid #e5e7eb;"; });
container.append(close, table);
const overlay = new Overlay({ element: container, positioning: "center-center", stopEvent: true });
overlay.set("id", FLOOD_PREDICTION_GRID_LAYER_ID);
overlay.setPosition(center);
map.addOverlay(overlay);
floodPredictionGridLayerRef.current = overlay;
} catch (error) {
console.error("침수예측지도 오버레이 표시 실패:", error);
}
@@ -5256,7 +5246,7 @@ const Map = () => {
zIndex: 9993,
});
layer.set("id", FLOOD_HISTORY_AREA_LAYER_ID);
layer.set("lyrNm", "침수흔적도");
layer.set("lyrNm", FLOOD_HISTORY_LAYER_NAME);
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
layer.set("clickPsblYn", "N");
gisMaps.current.addLayer(layer);
@@ -5276,7 +5266,7 @@ const Map = () => {
zIndex: 9992,
});
layer.set("id", FLOOD_HISTORY_DEPTH_LAYER_ID);
layer.set("lyrNm", "침수흔적도");
layer.set("lyrNm", FLOOD_HISTORY_LAYER_NAME);
layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE);
layer.set("clickPsblYn", "N");
gisMaps.current.addLayer(layer);
@@ -6699,7 +6689,7 @@ const Map = () => {
...baseLayerCode,
id: FLDNG_HSTRY_WFS_LAYER_ID,
lyrSn: FLDNG_HSTRY_WFS_LAYER_ID,
lyrNm: "침수흔적도",
lyrNm: FLOOD_HISTORY_LAYER_NAME,
lyrTyNm: FLOOD_HISTORY_LAYER_TYPE,
actlLyrYn: "Y",
exprsYn: "Y",
@@ -6710,7 +6700,7 @@ const Map = () => {
const layer = core?.addWfsLayer?.(layerCode);
layer?.set?.("id", FLDNG_HSTRY_WFS_LAYER_ID);
layer?.set?.("lyrNm", "침수흔적도");
layer?.set?.("lyrNm", FLOOD_HISTORY_LAYER_NAME);
layer?.set?.("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
return () => {
@@ -26,7 +26,8 @@ export const BASIC_TYPES = [
{ val: "tb_ms_swage_ppl", text: "하수관로" },
{ val: "tb_il_swage_prcs_fclt", text: "하수처리시설" },
{ val: "tb_ms_swage_prcs_zone", text: "하수처리구역" },
{ val: "tb_ms_fldng_wtlv_map", text: "침수흔적도" },
{ val: "tb_ms_fldng_wtlv_map", text: "침수흔적수위" },
{ val: "tb_ms_fldng_dpth_map", text: "침수흔적깊이" },
{ val: "tb_ms_swage_mhl", text: "하수맨홀" },
{ val: "tb_ms_swage_pmstn", text: "하수펌프장" },
{ val: "tb_ms_stofcbr", text: "우수토실" },
@@ -33,7 +33,7 @@
- 신규 파일과 함수에 화면/API 역할 한글 주석을 작성한다.
- 결과표는 `시도, 시군구, 분석시각, 강우시작시각, 관측 누적강수량, 예측시간, 예측 누적강수량, 초과 빈도(년), 침수위험도` 순서로 표시한다.
- 예측시간은 `N시간 후`, 관측·예측 누적강수량은 소수점 한 자리로 표시한다.
- 지도화면 분석도구는 공용 분석시각 범위와 `심각` 조건의 count API를 조회하여 `침수예측현황(x건)`으로 표시한다.
- 지도화면 분석도구는 공용 분석시각 범위와 `위험` 조건의 count API를 조회하여 `침수예측현황(x건)`으로 표시한다.
## 지도 연계
@@ -2,21 +2,22 @@
## 목적
침수예측현황 선택행으로 지도 이동 시 선택한 도시침수지도 중앙에 시도, 시군구, 분석시각, 강우시작시각, 관측 누적강수량(mm), 예측시각, 예측 누적강수량(mm), 초과 빈도(년), 침수위험도를 포함한 단일 오버레이를 표시한다. 격자별 라벨은 표시하지 않는다.
침수예측현황 선택행으로 지도 이동 시 선택한 도시침수지도 중앙에 사각 HTML table 단일 오버레이를 표시한다. 제목은 `[시도] [시군구] 침수예측` 두 열 병합이며, 본문은 분석시각, 강우시작시각, 관측 누적강수량(mm), 예측시각, 예측 누적강수량(mm), 초과 빈도(년), 침수위험도 순서다. 격자별 라벨과 위치 마커는 표시하지 않는다.
## 구현 방법
- 현황 팝업 메시지에 `analysisDateTime`을 추가한다.
- 지도 페이지에서 BE `grid-map` API를 호출한다.
- 기존 도시침수지도 extent 중심 좌표에 위 9개 표시 항목을 줄바꿈 라벨로 렌더링한다.
- 기존 도시침수지도 extent 중심 좌표에 위 9개 표시 항목을 table 행으로 렌더링한다.
- 초단기예보격자망 WFS 폴리곤은 기존 레이어 표현을 유지한다.
- 다음 선택 시 전용 중앙 overlay를 삭제하고 새 응답으로 갱신한다.
현재·예측 누적강우는 FE에서 격자별로 계산하지 않는다. BE가 반환하는 선택 시군구 분석행의 `obsrvn_acml_rn`, `predc_acml_rn`을 사용하며, 선택 도시침수지도 중앙의 단일 overlay에 표시한다.
현재·예측 누적강우는 FE에서 격자별로 계산하지 않는다. BE가 반환하는 선택 시군구 분석행의 `obsrvn_acml_rn`, `predc_acml_rn`을 사용하며, 선택 도시침수지도 중앙의 단일 table overlay에 표시한다.
## 검증
- 슬라이드 4의 선택 시군구 도시침수지도 중앙 정보 배치
- 다른 행 선택 시 중앙 overlay 교체
- 닫기 버튼 클릭 시 overlay 제거
- 기존 지도 extent 이동·침수위험 레이어 선택 유지
- API 오류 시 기존 지도 이동 기능 유지
@@ -0,0 +1,28 @@
# SFR-011-009 FE 침수흔적도-깊이도 연계 작업계획
## 1. 상태
- 승인 대기
- 구현 전 계획 문서
- 범위: `apps/sn` 내부
## 2. 화면 계획
- 기존 침수흔적도 기본정보 상세 영역 우측에 침수깊이지도 연도 버튼 목록을 추가한다.
- 버튼은 투명 배경, 테두리만 있는 라운드 형태로 기존 상세정보 UI와 조화되게 구현한다.
- 선택 연도의 깊이도 항목은 기본정보 아래 별도 div와 table로 표시한다.
- 상세 div 우측 상단에 `X` 닫기 버튼을 둔다.
- 지도 시설물 클릭과 침수발생이력 선택에서 동일 상태 모델을 사용한다.
- 현재 표시 중인 연도 버튼은 파란색으로 표시한다.
- 연도가 많으면 좌우 화살표로 연도 버튼 목록을 가로 스크롤한다.
- 연도 선택 시 해당 침수흔적도-깊이 feature를 지도에서 선택 상태로 표시한다.
## 3. 구현 후보
- `app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx`
- `app/(map-layout)/map/components/fldngHstry/**`
- 관련 침수흔적도 타입/API 호출부
## 4. 검증
- 연도 중복 제거·정렬, 선택 연도 파란색 상태, 좌우 스크롤, 연도 전환, 닫기, 빈 데이터, 지도 feature 선택, 양쪽 진입 경로 및 기존 상세정보 회귀를 브라우저에서 확인한다.
@@ -197,7 +197,7 @@ export const windowPopupDefinitions = {
fldngHstryPopup: {
key: "fldngHstryPopup",
title: "침수발생이력",
width: 1160,
width: 1700,
height: 820,
},
gisEditPopup: {