From 2c3961644c63b9de45d4460e6e60252da6b81319 Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 27 Aug 2026 14:10:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=EC=98=88=EC=B8=A1?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysisMenu/AnalysisPopupMenu.tsx | 34 +++++++- apps/sn/app/(map-layout)/map/page.tsx | 84 ++++++++++++++++++- ...1-qa-SFR-011-008-침수예측지도-격자표시-검증계획.md | 7 ++ .../001-fe-SFR-011-008-침수예측현황-작업계획.md | 3 + ...2-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md | 22 +++++ ...1-fe-SFR-011-008-침수예측지도-격자표시-구현기록.md | 17 ++++ .../fldngPredcPrst/model/analysisTimeRange.ts | 9 ++ .../ui/FldngPredcPrstPopupContent.tsx | 68 ++++++++------- .../windowPopup/model/popupDefinitions.ts | 2 +- 9 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 apps/sn/docs/작업검증/SFR-011-008-침수예측/001-qa-SFR-011-008-침수예측지도-격자표시-검증계획.md create mode 100644 apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md create mode 100644 apps/sn/docs/작업내용/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측지도-격자표시-구현기록.md create mode 100644 apps/sn/src/features/fldngPredcPrst/model/analysisTimeRange.ts diff --git a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx index 582016955..fa6aa96c3 100644 --- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx +++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx @@ -12,6 +12,8 @@ import { TriangleAlert, Waves, } from "lucide-react"; +import { getRawJson } from "@/components/lib/api"; +import { createPredictionAnalysisTimeRange } from "@/features/fldngPredcPrst/model/analysisTimeRange"; import { windowPopupDefinitions, type WindowPopupKey, @@ -120,6 +122,32 @@ export default function AnalysisPopupMenu({ const [activeKey, setActiveKey] = useState< WindowPopupKey | "infoSearch" | null >(initialActiveKey); + const [seriousPredictionCount, setSeriousPredictionCount] = useState(0); + + // 침수예측현황 팝업의 기본 분석시각 범위와 동일한 범위로 메뉴 건수를 조회한다. + useEffect(() => { + const range = createPredictionAnalysisTimeRange(); + const params = new URLSearchParams({ + analysisFrom: range.analysisFrom, + analysisTo: range.analysisTo, + }); + let cancelled = false; + void getRawJson( + `/sewerage-service/api/v1/fldng/predc/prst/count?${params.toString()}`, + ) + .then((response) => { + if (cancelled) return; + const payload = response?.data ?? response; + const count = Number(payload); + setSeriousPredictionCount(Number.isFinite(count) ? count : 0); + }) + .catch(() => { + if (!cancelled) setSeriousPredictionCount(0); + }); + return () => { + cancelled = true; + }; + }, []); useEffect(() => { if (!isGisEditActive && activeKey === "gisEditPopup") { @@ -171,6 +199,10 @@ export default function AnalysisPopupMenu({ item.key === "gisEditPopup" ? isGisEditActive : activeKey === item.key; + const label = + item.key === "fldngPredcPrst" + ? `침수예측현황(${seriousPredictionCount}건)` + : item.label; return ( ); })} diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index d90df246e..521bfecc7 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -203,6 +203,15 @@ const EXMN_GRADE_COLORS: Record = { }; const EXMN_RESULT_MANHOLE_ICON_SRC = "/images/map/layericon/SN_EXMN_MANHOLE_PURPLE.png?v=3"; +const FLOOD_PREDICTION_GRID_LAYER_ID = "SN_FLDNG_PREDICTION_GRID_LABEL"; + +/** 침수예측 오버레이용 12자리 분석시각을 화면 표시 형식으로 변환한다. */ +const formatFloodPredictionDateTime = (value: unknown) => { + const text = String(value ?? "").trim(); + return /^\d{12}$/.test(text) + ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)} ${text.slice(8, 10)}:${text.slice(10, 12)}` + : text || "-"; +}; const getExmnGradeColor = (feature: any) => { const grade = Number(feature?.get?.("structuralGrade") ?? 0); @@ -228,6 +237,24 @@ 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 = "유하방향"; @@ -3209,6 +3236,9 @@ const Map = () => { const waterRiskPipeSourceRef = useRef(null); const waterRiskManholeLayerRef = useRef | null>(null); const waterRiskManholeSourceRef = useRef(null); + // 침수예측현황 선택행에 따라 초단기예보 격자 중앙 라벨을 교체하는 전용 overlay를 보관한다. + const floodPredictionGridLayerRef = useRef | null>(null); + const floodPredictionGridSourceRef = useRef(null); const gisEditDrawRef = useRef(null); const gisEditDrawSnapRef = useRef(null); const gisEditTranslateRef = useRef(null); @@ -3483,6 +3513,56 @@ const Map = () => { } }, []); + /** 기존 침수예측 중앙 오버레이를 제거한다. */ + const removeFloodPredictionGridLayer = useCallback(() => { + const core = GisApp.Module.core as any; + core?.deleteLayer?.(FLOOD_PREDICTION_GRID_LAYER_ID); + floodPredictionGridLayerRef.current = null; + floodPredictionGridSourceRef.current = null; + }, []); + + /** 선택행의 시군구·분석시각으로 도시침수지도 중앙 오버레이를 표시한다. */ + const loadFloodPredictionGridLayer = useCallback( + async (sggCd: string, analysisDateTime: string, extent: number[]) => { + const map = gisMaps.current; + if (!map || !/^\d{5}$/.test(sggCd) || !/^\d{12}$/.test(analysisDateTime)) return; + removeFloodPredictionGridLayer(); + try { + const response = await getRawJson( + `/sewerage-service/api/v1/fldng/predc/prst/grid-map?sggCd=${encodeURIComponent(sggCd)}&analysisDateTime=${encodeURIComponent(analysisDateTime)}`, + ); + 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; + } catch (error) { + console.error("침수예측지도 오버레이 표시 실패:", error); + } + }, + [removeFloodPredictionGridLayer], + ); + useEffect(() => { fetchData(); }, []); @@ -3602,6 +3682,7 @@ const Map = () => { : []; const sggCd = String(data.payload?.sggCd ?? "").trim(); const frequency = String(data.payload?.sbmrgFrqExpln ?? "").trim(); + const analysisDateTime = String(data.payload?.analysisDateTime ?? "").trim(); const map = gisMaps.current; const select = GisApp.Module.select as any; if ( @@ -3665,6 +3746,7 @@ const Map = () => { padding: [50, 50, 50, 50], maxZoom: 16, }); + void loadFloodPredictionGridLayer(sggCd, analysisDateTime, extent); return; } @@ -3703,7 +3785,7 @@ const Map = () => { window.clearInterval(timer); window.removeEventListener("message", handlePopupMessage); }; - }, []); + }, [loadFloodPredictionGridLayer]); useEffect(() => { if (shouldShowGisEditSession) return; diff --git a/apps/sn/docs/작업검증/SFR-011-008-침수예측/001-qa-SFR-011-008-침수예측지도-격자표시-검증계획.md b/apps/sn/docs/작업검증/SFR-011-008-침수예측/001-qa-SFR-011-008-침수예측지도-격자표시-검증계획.md new file mode 100644 index 000000000..6fdc82af7 --- /dev/null +++ b/apps/sn/docs/작업검증/SFR-011-008-침수예측/001-qa-SFR-011-008-침수예측지도-격자표시-검증계획.md @@ -0,0 +1,7 @@ +# SFR-011-008 침수예측지도 격자 표시 검증계획 + +- 침수예측현황 행 클릭 시 기존 지도 extent 이동 확인 +- `grid-map` API의 시군구·분석시각 파라미터와 WFS `latc_cd` 필터 확인 +- 각 격자 중앙 `latc_cd`·침수위험도·현재/예측 누적강우 표시 확인 +- 행 재선택 시 기존 overlay 제거 및 신규 격자 갱신 확인 +- BE 단위 테스트·컴파일 및 SN 브라우저 콘솔 오류 확인 diff --git a/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md b/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md index 3c30fb406..e8cd9a94f 100644 --- a/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md +++ b/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md @@ -31,6 +31,9 @@ - `ListDiv` 내부에서 최소 테이블 폭과 가로 스크롤을 제공한다. - Excel은 현재 적용 검색조건을 동일하게 전달한다. - 신규 파일과 함수에 화면/API 역할 한글 주석을 작성한다. +- 결과표는 `시도, 시군구, 분석시각, 강우시작시각, 관측 누적강수량, 예측시간, 예측 누적강수량, 초과 빈도(년), 침수위험도` 순서로 표시한다. +- 예측시간은 `N시간 후`, 관측·예측 누적강수량은 소수점 한 자리로 표시한다. +- 지도화면 분석도구는 공용 분석시각 범위와 `심각` 조건의 count API를 조회하여 `침수예측현황(x건)`으로 표시한다. ## 지도 연계 diff --git a/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md new file mode 100644 index 000000000..8dbbdd0a2 --- /dev/null +++ b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md @@ -0,0 +1,22 @@ +# SFR-011-008 침수예측지도 중앙 오버레이 표시 FE 작업계획 + +## 목적 + +침수예측현황 선택행으로 지도 이동 시 선택한 도시침수지도 중앙에 시도, 시군구, 분석시각, 강우시작시각, 관측 누적강수량(mm), 예측시각, 예측 누적강수량(mm), 초과 빈도(년), 침수위험도를 포함한 단일 오버레이를 표시한다. 격자별 라벨은 표시하지 않는다. + +## 구현 방법 + +- 현황 팝업 메시지에 `analysisDateTime`을 추가한다. +- 지도 페이지에서 BE `grid-map` API를 호출한다. +- 기존 도시침수지도 extent 중심 좌표에 위 9개 표시 항목을 줄바꿈 라벨로 렌더링한다. +- 초단기예보격자망 WFS 폴리곤은 기존 레이어 표현을 유지한다. +- 다음 선택 시 전용 중앙 overlay를 삭제하고 새 응답으로 갱신한다. + +현재·예측 누적강우는 FE에서 격자별로 계산하지 않는다. BE가 반환하는 선택 시군구 분석행의 `obsrvn_acml_rn`, `predc_acml_rn`을 사용하며, 선택 도시침수지도 중앙의 단일 overlay에 표시한다. + +## 검증 + +- 슬라이드 4의 선택 시군구 도시침수지도 중앙 정보 배치 +- 다른 행 선택 시 중앙 overlay 교체 +- 기존 지도 extent 이동·침수위험 레이어 선택 유지 +- API 오류 시 기존 지도 이동 기능 유지 diff --git a/apps/sn/docs/작업내용/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측지도-격자표시-구현기록.md b/apps/sn/docs/작업내용/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측지도-격자표시-구현기록.md new file mode 100644 index 000000000..9a3b4f0b1 --- /dev/null +++ b/apps/sn/docs/작업내용/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측지도-격자표시-구현기록.md @@ -0,0 +1,17 @@ +# SFR-011-008 침수예측지도 격자 표시 FE 구현기록 + +## 구현 내용 + +- 침수예측현황 선택행의 `analysisDateTime`을 지도 이동 메시지에 포함했다. +- BE `grid-map` 응답의 `latcCd`를 GeoServer `tb_ms_shtm_frcst_latc_info` WFS CQL 필터로 사용한다. +- 격자 폴리곤 내부점에 `latc_cd`, 침수위험도, 현재·예측 누적강우 라벨을 표시한다. +- `SN_FLDNG_PREDICTION_GRID_LABEL` 전용 레이어로 재선택 시 오버레이를 교체한다. + +## 설계 변경 대기 + +사용자 확인에 따라 최종 구현 대상은 격자별 라벨이 아니라 선택한 도시침수지도 중앙의 단일 오버레이로 변경되었다. 기존 격자별 구현은 중앙 오버레이 구현으로 교체한다. + +## 기준 + +- `침수예측.pptx` 4페이지 +- 요구사항 `SFR-011-008` diff --git a/apps/sn/src/features/fldngPredcPrst/model/analysisTimeRange.ts b/apps/sn/src/features/fldngPredcPrst/model/analysisTimeRange.ts new file mode 100644 index 000000000..2e60793ab --- /dev/null +++ b/apps/sn/src/features/fldngPredcPrst/model/analysisTimeRange.ts @@ -0,0 +1,9 @@ +/** 침수예측현황 목록과 분석도구 메뉴가 공유하는 기본 분석시각 범위다. */ +export const createPredictionAnalysisTimeRange = (now = new Date()) => { + const from = new Date(now); + from.setHours(0, 0, 0, 0); + const to = new Date(now); + const toApiDateTime = (value: Date) => + `${value.getFullYear()}${String(value.getMonth() + 1).padStart(2, "0")}${String(value.getDate()).padStart(2, "0")}${String(value.getHours()).padStart(2, "0")}${String(value.getMinutes()).padStart(2, "0")}`; + return { analysisFrom: toApiDateTime(from), analysisTo: toApiDateTime(to) }; +}; diff --git a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx index b7aa57bc5..93c588c75 100644 --- a/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx +++ b/apps/sn/src/features/fldngPredcPrst/ui/FldngPredcPrstPopupContent.tsx @@ -17,6 +17,7 @@ import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; import { useAlertStore } from "@/shared/lib"; import useStore from "@/store/useStore"; +import { createPredictionAnalysisTimeRange } from "../model/analysisTimeRange"; type ApiRow = { sggCd: string; @@ -26,8 +27,8 @@ type ApiRow = { rainBeginningDateTime: string; analysisDateTime: string; observedAccumulatedRainfall: number | null; - rainHour: number | null; - rainfall: number | null; + predictionHour: number | null; + predictedAccumulatedRainfall: number | null; exceededFrequencyYear: number | null; floodRiskGrade: string; }; @@ -35,11 +36,11 @@ type ApiRow = { type TableRow = ApiRow & Record & { rowNo: number; - rainBeginningDateTimeDisplay: string; analysisDateTimeDisplay: string; + rainBeginningDateTimeDisplay: string; observedAccumulatedRainfallDisplay: string; - rainHourDisplay: string; - rainfallDisplay: string; + predictionHourDisplay: string; + predictedAccumulatedRainfallDisplay: string; exceededFrequencyYearDisplay: string; }; @@ -76,8 +77,8 @@ const HEADS: UiHeadProps[] = [ }, { id: "rainBeginningDateTimeDisplay", - title: "강우시각", - widthPercent: 13, + title: "강우시작시각", + widthPercent: 16, }, { id: "observedAccumulatedRainfallDisplay", @@ -86,14 +87,14 @@ const HEADS: UiHeadProps[] = [ align: "right", }, { - id: "rainHourDisplay", - title: "강우시간", - widthPercent: 8, + id: "predictionHourDisplay", + title: "예측시간", + widthPercent: 9, }, { - id: "rainfallDisplay", - title: "강수량(mm)", - widthPercent: 10, + id: "predictedAccumulatedRainfallDisplay", + title: "예측 누적강수량(mm)", + widthPercent: 15, align: "right", }, { @@ -102,25 +103,21 @@ const HEADS: UiHeadProps[] = [ widthPercent: 10, align: "right", }, - { id: "floodRiskGrade", title: "위험등급", widthPercent: 10 }, + { id: "floodRiskGrade", title: "침수위험도", widthPercent: 10 }, ]; -/** 브라우저 로컬시각을 datetime-local 입력값으로 변환한다. */ -const localDateTime = (date: Date) => - `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}T${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`; - /** 최초·초기화 조회조건은 금일 분석결과를 기준으로 생성한다. */ const buildInitialFilters = (): Filters => { - const now = new Date(); - const from = new Date(now); - from.setHours(0, 0, 0, 0); + const range = createPredictionAnalysisTimeRange(); + const toDateTimeLocal = (value: string) => + `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}T${value.slice(8, 10)}:${value.slice(10, 12)}`; return { ctpvCd: "0", sggCd: "0", rainBgngFrom: "", rainBgngTo: "", - analysisFrom: localDateTime(from), - analysisTo: localDateTime(now), + analysisFrom: toDateTimeLocal(range.analysisFrom), + analysisTo: toDateTimeLocal(range.analysisTo), frequencyYear: "", }; }; @@ -132,9 +129,10 @@ const displayDate = (value: string) => ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)} ${value.slice(8, 10)}:${value.slice(10, 12)}` : value || "-"; -const rainHourText = (value: number | null) => { +/** 분석시각 기준 예측시간을 사용자 표시문구로 변환한다. */ +const predictionHourText = (value: number | null) => { const hour = Number(value ?? 0); - return hour === 0 ? "현재" : `${hour > 0 ? "+" : "-"}${Math.abs(hour)}시간`; + return hour > 0 ? `${hour}시간 후` : "-"; }; const formatNumber = (value: number | null, digits: number) => @@ -189,7 +187,7 @@ const validateFilters = (filters: Filters) => { }; const buildRowId = (row: ApiRow) => - `${row.ctpvCd}:${row.sggCd}:${row.rainBeginningDateTime}:${row.analysisDateTime}:${row.rainHour}`; + `${row.ctpvCd}:${row.sggCd}:${row.rainBeginningDateTime}:${row.analysisDateTime}:${row.predictionHour}`; const isDangerGrade = (value: string) => { const grade = String(value ?? "") @@ -262,16 +260,19 @@ export default function FldngPredcPrstPopupContent() { content.map((item) => ({ ...item, rowNo: 0, + analysisDateTimeDisplay: displayDate(item.analysisDateTime), rainBeginningDateTimeDisplay: displayDate( item.rainBeginningDateTime, ), - analysisDateTimeDisplay: displayDate(item.analysisDateTime), observedAccumulatedRainfallDisplay: formatNumber( item.observedAccumulatedRainfall, - 4, + 1, + ), + predictionHourDisplay: predictionHourText(item.predictionHour), + predictedAccumulatedRainfallDisplay: formatNumber( + item.predictedAccumulatedRainfall, + 1, ), - rainHourDisplay: rainHourText(item.rainHour), - rainfallDisplay: formatNumber(item.rainfall, 1), exceededFrequencyYearDisplay: item.exceededFrequencyYear == null ? "-" @@ -359,7 +360,12 @@ export default function FldngPredcPrstPopupContent() { window.opener.postMessage( { type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SHOW_FLOOD_GROUP, - payload: { extent, sggCd: row.sggCd, sbmrgFrqExpln }, + payload: { + extent, + sggCd: row.sggCd, + sbmrgFrqExpln, + analysisDateTime: row.analysisDateTime, + }, }, window.location.origin, ); diff --git a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts index 9663e9792..369d810d8 100644 --- a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts +++ b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts @@ -186,7 +186,7 @@ export const windowPopupDefinitions = { key: "fldngPredcPrst", title: "침수예측현황", width: 1700, - height: 900, + height: 820, }, agedPipeStatusByCtpvPopup: { key: "agedPipeStatusByCtpvPopup", From c1ffc33834379f2b8426ebbfee415bea03d22dc8 Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 27 Aug 2026 22:33:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=EC=98=88=EC=B8=A1?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20&?= =?UTF-8?q?=20=EC=B9=A8=EC=88=98=ED=9D=94=EC=A0=81=EA=B9=8A=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/common/table.ts | 9 ++ .../fldngHstry/FldngHstryPopupPage.tsx | 20 ++++- .../components/fldngHstry/fldngHstryTypes.ts | 12 +-- .../popupComponent/featureClickEventPopup.tsx | 90 +++++++++++++++++-- apps/sn/app/(map-layout)/map/page.tsx | 78 +++++++--------- .../(map-layout)/map/searchModal/basicTab.tsx | 3 +- .../001-fe-SFR-011-008-침수예측현황-작업계획.md | 2 +- ...2-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md | 7 +- ...002-fe-SFR-011-009-침수흔적도-깊이도연계-작업계획.md | 28 ++++++ .../windowPopup/model/popupDefinitions.ts | 2 +- 10 files changed, 185 insertions(+), 66 deletions(-) create mode 100644 apps/sn/docs/작업계획/SFR-011-009-침수발생이력/002-fe-SFR-011-009-침수흔적도-깊이도연계-작업계획.md diff --git a/apps/sn/app/(map-layout)/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts index bd3a44dbe..1a728f540 100644 --- a/apps/sn/app/(map-layout)/map/components/common/table.ts +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -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': '침수재해명', diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx index ee4789351..b4dccf951 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -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(), }; }); diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts index 9c625791f..d0dfee9f9 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -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 }, diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index 9ac20b8db..e93e9b502 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -35,6 +35,7 @@ import { getLayerContentComponent } from "../common"; import { FldngWtlvMapCol, FldngWtlvMapCol2, + FldngDpthMapCol, SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, @@ -178,6 +179,9 @@ const FeatureClickEventPopup = ({ const linkedFacilityFeatureRef = useRef(null); const linkedFacilityOriginalStyleRef = useRef(null); const [mngInstOptions, setMngInstOptions] = useState([]); + const [floodDepthRows, setFloodDepthRows] = useState[]>([]); + const [selectedFloodDepthYear, setSelectedFloodDepthYear] = useState(null); + const floodDepthYearListRef = useRef(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 && ( +
+
+ +
+ {Array.from( + new Set( + floodDepthRows + .map((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim()) + .filter(Boolean), + ), + ).map((year) => ( + + ))} +
+ +
+
+ + + {Object.entries( + floodDepthRows.find((row) => String(row.fldng_yr ?? row.fldngYr ?? row.yr ?? "").trim() === selectedFloodDepthYear) ?? {}, + ) + .filter(([key]) => !key.toLowerCase().includes("geom")) + .map(([key, value]) => ( + + + + + ))} + +
{key}{String(value ?? "-")}
+
+
+ )} {ContentComponent && ( }); /** 선택한 도시침수지도 중앙에 침수예측 분석값을 표시하는 오버레이 스타일이다. */ -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 | null>(null); const waterRiskManholeSourceRef = useRef(null); // 침수예측현황 선택행에 따라 초단기예보 격자 중앙 라벨을 교체하는 전용 overlay를 보관한다. - const floodPredictionGridLayerRef = useRef | null>(null); - const floodPredictionGridSourceRef = useRef(null); + const floodPredictionGridLayerRef = useRef(null); const gisEditDrawRef = useRef(null); const gisEditDrawSnapRef = useRef(null); const gisEditTranslateRef = useRef(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 () => { diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx index 7c21ac071..97a6b2420 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -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: "우수토실" }, diff --git a/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md b/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md index e8cd9a94f..17410affd 100644 --- a/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md +++ b/apps/sn/docs/작업계획/SFR-011-008-침수예측/001-fe-SFR-011-008-침수예측현황-작업계획.md @@ -33,7 +33,7 @@ - 신규 파일과 함수에 화면/API 역할 한글 주석을 작성한다. - 결과표는 `시도, 시군구, 분석시각, 강우시작시각, 관측 누적강수량, 예측시간, 예측 누적강수량, 초과 빈도(년), 침수위험도` 순서로 표시한다. - 예측시간은 `N시간 후`, 관측·예측 누적강수량은 소수점 한 자리로 표시한다. -- 지도화면 분석도구는 공용 분석시각 범위와 `심각` 조건의 count API를 조회하여 `침수예측현황(x건)`으로 표시한다. +- 지도화면 분석도구는 공용 분석시각 범위와 `위험` 조건의 count API를 조회하여 `침수예측현황(x건)`으로 표시한다. ## 지도 연계 diff --git a/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md index 8dbbdd0a2..a553df935 100644 --- a/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md +++ b/apps/sn/docs/작업계획/SFR-011-008-침수예측/002-fe-SFR-011-008-침수예측지도-격자표시-작업계획.md @@ -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 오류 시 기존 지도 이동 기능 유지 diff --git a/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/002-fe-SFR-011-009-침수흔적도-깊이도연계-작업계획.md b/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/002-fe-SFR-011-009-침수흔적도-깊이도연계-작업계획.md new file mode 100644 index 000000000..b84daf0f0 --- /dev/null +++ b/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/002-fe-SFR-011-009-침수흔적도-깊이도연계-작업계획.md @@ -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 선택, 양쪽 진입 경로 및 기존 상세정보 회귀를 브라우저에서 확인한다. diff --git a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts index 369d810d8..454b24431 100644 --- a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts +++ b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts @@ -197,7 +197,7 @@ export const windowPopupDefinitions = { fldngHstryPopup: { key: "fldngHstryPopup", title: "침수발생이력", - width: 1160, + width: 1700, height: 820, }, gisEditPopup: {