From 8a35d232efcff99dc30b41a33066d6b7164b99b9 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 24 Jul 2026 14:22:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=ED=8C=9D=EC=97=85=20api=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FldngHstryDetailPanel.tsx} | 4 +- .../fldngHstry/FldngHstryPopupPage.tsx | 378 +++++++++++++++ .../components/fldngHstry/fldngHstryTypes.ts | 446 +++++++++++++++++ .../floodHistory/FloodHistoryPopupPage.tsx | 408 ---------------- .../floodHistory/floodHistoryTypes.ts | 458 ------------------ apps/sn/app/(map-layout)/map/page.tsx | 6 +- .../001-fe-SFR-011-009-침수발생이력-작업카드.md | 8 +- .../001-fe-SFR-011-009-침수발생이력-통합구현기록.md | 8 +- .../001-fe-SN-RDII분석결과-지도연계유지보수-통합구현기록.md | 2 +- .../windowPopup/model/popupDefinitions.ts | 4 +- apps/sn/src/features/windowPopup/registry.tsx | 10 +- .../windowPopup/ui/FldngHstryPopupContent.tsx | 5 + .../ui/FloodHistoryPopupContent.tsx | 5 - 13 files changed, 850 insertions(+), 892 deletions(-) rename apps/sn/app/(map-layout)/map/components/{floodHistory/FloodHistoryDetailPanel.tsx => fldngHstry/FldngHstryDetailPanel.tsx} (97%) create mode 100644 apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx create mode 100644 apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts delete mode 100644 apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx delete mode 100644 apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts create mode 100644 apps/sn/src/features/windowPopup/ui/FldngHstryPopupContent.tsx delete mode 100644 apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx diff --git a/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx similarity index 97% rename from apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx rename to apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx index 66ed55078..a56aa7127 100644 --- a/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useMemo, useState } from "react"; -import { formatFloodArea, formatFloodDepth, type FloodHistoryItem } from "./floodHistoryTypes"; +import { formatFloodArea, formatFloodDepth, type FloodHistoryItem } from "./fldngHstryTypes"; type Props = { detail: FloodHistoryItem; @@ -35,7 +35,7 @@ const renderRows = (rows: Array<[string, string]>) => ( ); -export default function FloodHistoryDetailPanel({ detail, onClose, position }: Props) { +export default function FldngHstryDetailPanel({ detail, onClose, position }: Props) { // 지도에서 침수흔적도 polygon을 클릭했을 때, 상세정보는 업무 의미 기준으로 3개 탭에 나눠 보여준다. const [activeTab, setActiveTab] = useState("year"); diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx new file mode 100644 index 000000000..147fafdb2 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -0,0 +1,378 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Input } from "@common/components"; +import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui"; +import type { UiTableSort } from "@common/business_components/ui"; +import { getRawJson } from "@/components/lib/api"; +import useStore from "@/store/useStore"; +import LoadingOverlay from "@/components/ui/LoadingBar"; +import { + attachFloodHistoryGeometry, + buildFloodHistoryListQueryString, + buildFloodHistoryYearOptions, + FLOOD_HISTORY_DEFAULT_FILTERS, + FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE, + FLOOD_HISTORY_DOWNLOAD_HEADERS, + FLOOD_HISTORY_LIST_HEADLIST, + FLOOD_HISTORY_PAGE_SIZE, + FLOOD_HISTORY_ROW_SELECT_MESSAGE, + normalizeFloodHistoryFilters, + toFloodHistoryTableRows, + type FloodHistoryApiItem, + type FloodHistoryFilters, + type FloodHistoryTableRow, +} from "./fldngHstryTypes"; + +const unwrapPayload = (response: any): T => + (response?.data ?? response) as T; + +const resolveOptionText = ( + code: string, + options: Array<{ text: string; val: string | number }>, + prefixLength?: number, +) => { + const normalized = String(code ?? "").trim(); + if (!normalized || normalized === "0") return ""; + const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized; + return options.find((item) => String(item.val).trim() === lookup)?.text ?? ""; +}; + +const resolveFloodHistoryApiSort = (sort?: UiTableSort) => { + if (!sort?.key || !sort.direction) return undefined; + return { key: sort.key, direction: sort.direction } as UiTableSort; +}; + +const buildDownloadRows = (rows: FloodHistoryTableRow[]) => + rows.map((row) => ({ + rowNo: row.rowNo, + floodYearDisplay: row.floodYearDisplay, + ctpvNm: row.ctpvNm, + sggNm: row.sggNm, + disasterName: row.disasterName, + rainBgngTmDisplay: row.rainBgngTmDisplay, + rainEndTmDisplay: row.rainEndTmDisplay, + rainTotDisplay: row.rainTotDisplay, + rainStrDisplay: row.rainStrDisplay, + floodHourDisplay: row.floodHourDisplay, + districtName: row.districtName, + causeDetailCode: row.causeDetailCode, + causeDetailName: row.causeDetailName, + avgFloodDepthDisplay: row.avgFloodDepthDisplay, + floodAreaDisplay: row.floodAreaDisplay, + exmnYearDisplay: row.exmnYearDisplay, + damageAmountDisplay: row.damageAmountDisplay, + })); + +const resolveRegionNames = ( + row: FloodHistoryTableRow, + ctpvOptions: Array<{ text: string; val: string | number }>, + sggOptions: Array<{ text: string; val: string | number }>, +): FloodHistoryTableRow => ({ + ...row, + ctpvNm: resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) || row.ctpvNm, + sggNm: resolveOptionText(row.sggCd ?? "", sggOptions, 5) || row.sggNm, +}); + +export default function FldngHstryPopupPage() { + const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); + const [filters, setFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); + const [appliedFilters, setAppliedFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); + const [rows, setRows] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [selectedIndex, setSelectedIndex] = useState(-1); + const [isLoading, setIsLoading] = useState(false); + const [sort, setSort] = useState(); + const [appliedSort, setAppliedSort] = useState(); + + useEffect(() => { + if (ctpv.length === 0) void fetchCtpv(); + if (sgg.length === 0) void fetchSgg(); + }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); + + useEffect(() => { + if (rows.length === 0) return; + if (ctpv.length === 0 && sgg.length === 0) return; + + setRows((current) => + current.map((row) => resolveRegionNames(row, ctpv as any, sgg as any)), + ); + }, [ctpv, rows.length, sgg]); + + const floodYearOptions = useMemo(() => buildFloodHistoryYearOptions(), []); + + const sggOptions = useMemo( + () => + filters.ctpvCd === "0" + ? sgg + : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))), + [filters.ctpvCd, sgg], + ); + + const fetchRows = useCallback( + async (page: number, nextFilters: FloodHistoryFilters, size: number, nextSort?: UiTableSort) => { + const normalized = normalizeFloodHistoryFilters(nextFilters); + const queryString = buildFloodHistoryListQueryString( + normalized, + page, + size, + resolveFloodHistoryApiSort(nextSort), + ); + const response = await getRawJson(`/sewerage-service/api/v1/fldng/hstry?${queryString}`); + const payload = unwrapPayload<{ items?: FloodHistoryApiItem[]; total?: number }>(response); + const items = Array.isArray(payload?.items) ? payload.items : []; + + const displayItems = items.map((item) => { + const hydrated = attachFloodHistoryGeometry(item); + return { + ...hydrated, + ctpvNm: resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2), + sggNm: resolveOptionText(hydrated.sggCd ?? "", sgg as any, 5), + }; + }); + + return { + rows: toFloodHistoryTableRows(displayItems, page, size), + total: Number(payload?.total ?? 0), + }; + }, + [ctpv, sgg], + ); + + const loadRows = useCallback( + async (page: number, nextFilters: FloodHistoryFilters, nextSort?: UiTableSort) => { + setIsLoading(true); + try { + const normalized = normalizeFloodHistoryFilters(nextFilters); + const response = await fetchRows(page, normalized, FLOOD_HISTORY_PAGE_SIZE, nextSort); + setRows(response.rows); + setTotal(response.total); + setCurrentPage(page); + setAppliedFilters(normalized); + setSort(nextSort); + setAppliedSort(nextSort); + setSelectedIndex(-1); + } catch (error) { + console.error("침수발생이력 목록 조회 중 오류:", error); + setRows([]); + setTotal(0); + alert(error instanceof Error ? error.message : "침수발생이력 목록 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }, + [fetchRows], + ); + + useEffect(() => { + void loadRows(1, FLOOD_HISTORY_DEFAULT_FILTERS, undefined); + // 최초 로딩은 한 번만 수행한다. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSearch = async (page = 1) => { + const normalized = normalizeFloodHistoryFilters(filters); + + if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") { + alert("시도를 먼저 선택하세요."); + return; + } + + setFilters(normalized); + await loadRows(page, normalized, sort); + }; + + const handlePageChange = async (page: number) => { + await loadRows(page, appliedFilters, appliedSort); + }; + + const handleSortChange = async (nextSort?: UiTableSort) => { + await loadRows(1, appliedFilters, nextSort); + }; + + const handleRowClick = (index?: number) => { + if (typeof index !== "number") return; + const row = rows[index]; + if (!row) return; + + setSelectedIndex(index); + window.opener?.postMessage( + { + type: FLOOD_HISTORY_ROW_SELECT_MESSAGE, + row, + openDetail: false, + }, + window.location.origin, + ); + }; + + const handleDownload = async () => { + if (total === 0) { + alert("다운로드할 침수발생이력 데이터가 없습니다."); + return; + } + + setIsLoading(true); + try { + const response = await fetchRows( + 1, + appliedFilters, + Math.max(total, FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE), + appliedSort, + ); + await downloadExcelWithKoreanHeaders({ + data: buildDownloadRows(response.rows) as unknown as JSON[], + fileName: `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`, + sheetNameArray: ["침수발생이력"], + headerRange: "A1:Q1", + headerMapping: FLOOD_HISTORY_DOWNLOAD_HEADERS, + }); + } catch (error) { + console.error("침수발생이력 다운로드 중 오류:", error); + alert(error instanceof Error ? error.message : "침수발생이력 다운로드 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+

침수발생이력

+
+
+ +
+
+ + + + + + + + +
+ + +
+
+ +
+ + headName={<>침수발생이력 조회 결과 총 {total.toLocaleString()}건} + tableData={rows} + headlist={FLOOD_HISTORY_LIST_HEADLIST} + pageSize={FLOOD_HISTORY_PAGE_SIZE} + total={total} + cellClick={(index) => handleRowClick(index)} + onPageChange={(page) => void handlePageChange(page)} + curPage={currentPage} + index={selectedIndex} + widthFixed={true} + isHideCount={true} + sort={sort} + onSortChange={(nextSort) => void handleSortChange(nextSort)} + /> +
+ +
+ 행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다. +
+
+
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts new file mode 100644 index 000000000..9812c2937 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -0,0 +1,446 @@ +"use client"; + +import type { UiHeadProps, UiTableSort } from "@common/business_components/ui"; + +export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup"; +export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; +export const FLOOD_HISTORY_LAYER_TYPE = "tb_sn_flood_trace_mock"; +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 FLOOD_HISTORY_PAGE_SIZE = 10; +export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE = 2000; + +export type FloodHistoryFilters = { + ctpvCd: string; + sggCd: string; + floodYear: string; + disasterName: string; +}; + +export type FloodHistoryDepthZone = { + zoneId: string; + depthLabel: string; + color: string; + ring: [number, number][]; +}; + +export type FloodHistoryApiItem = { + fid: string; + fldngMapFid?: string; + fldngWtlvMapSn?: number | null; + taskClCd?: string; + fldngDstrNm?: string; + exmnYr?: string; + avgFldngWtlv?: number | null; + fldngArea?: number | null; + dtlOcrnCsCn?: string; + stdgCd?: string; + ctpvCd?: string; + sggCd?: string; + dataSn?: number | null; + bgngYmd?: string; + endYmd?: string; + fldngBgngHm?: string; + fldngEndHm?: string; + fldngCsDtlCd?: string; + fldngDstrctNm?: string; + damAmt?: number | null; + fldngHr?: number | null; + fldngYr?: string; + cstrnFbctnYn?: string; + atchFileSn?: number | null; + confArea?: number | null; + confLen?: number | null; + rainBgngTm?: string | null; + rainEndTm?: string | null; + rainTot?: number | null; + rainStr?: number | null; +}; + +export type FloodHistoryItem = FloodHistoryApiItem & { + historyId: string; + ctpvNm: string; + sggNm: string; + floodYear: string; + exmnYear: string; + disasterName: string; + districtName: string; + floodAreaHa: number; + avgFloodDepthM: number; + floodDepthM: number; + gradeLabel: string; + startDate: string; + endDate: string; + startTime: string; + endTime: string; + floodHour: string; + causeDetailCode: string; + causeDetailName: string; + damageAmount: string; + rainStartDateTime: string; + rainEndDateTime: string; + rainStartTime: string; + rainEndTime: string; + totalRainfallMm: number; + rainfallIntensityMmHr: number; + areaRing: [number, number][]; + depthZones: FloodHistoryDepthZone[]; +}; + +export type FloodHistoryTableRow = FloodHistoryItem & { + rowNo: number; + floodYearDisplay: string; + floodHourDisplay: string; + avgFloodDepthDisplay: string; + floodAreaDisplay: string; + exmnYearDisplay: string; + damageAmountDisplay: string; + rainBgngTmDisplay: string; + rainEndTmDisplay: string; + rainTotDisplay: string; + rainStrDisplay: string; +}; + +export const FLOOD_HISTORY_DEFAULT_FILTERS: FloodHistoryFilters = { + ctpvCd: "0", + sggCd: "0", + floodYear: "", + disasterName: "", +}; + +export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [ + { color: "#fff27a", label: "0.0~0.5m" }, + { color: "#c2f13d", label: "0.5~1.0m" }, + { color: "#67d8ff", label: "1.0~2.0m" }, + { color: "#8f8bff", label: "2.0~5.0m" }, + { color: "#f2528b", label: "5.0m 이상" }, +] as const; + +export const FLOOD_HISTORY_LIST_HEADLIST: UiHeadProps[] = [ + { id: "rowNo", title: "NO", widthPercent: 5, sortable: false }, + { id: "floodYearDisplay", title: "침수년도", widthPercent: 6, sortable: true, sortKey: "fldngYr" }, + { id: "ctpvNm", title: "시도", widthPercent: 7, sortable: true, sortKey: "ctpvCd" }, + { id: "sggNm", title: "시군구", widthPercent: 8, sortable: true, sortKey: "sggCd" }, + { id: "disasterName", title: "침수재해명", widthPercent: 14, sortable: true, sortKey: "fldngDstrNm" }, + { id: "rainBgngTmDisplay", title: "강우시작일시", widthPercent: 10, sortable: true, sortKey: "rainBgngTm" }, + { id: "rainEndTmDisplay", title: "강우종료일시", widthPercent: 10, sortable: true, sortKey: "rainEndTm" }, + { id: "rainTotDisplay", title: "총강우량(mm)", widthPercent: 8, sortable: true, sortKey: "rainTot" }, + { id: "rainStrDisplay", title: "강우강도(mm/hr)", widthPercent: 8, sortable: true, sortKey: "rainStr" }, + { id: "floodHourDisplay", title: "침수시간", widthPercent: 7, sortable: true, sortKey: "fldngHr" }, + { id: "districtName", title: "침수지구명", widthPercent: 10, sortable: true, sortKey: "fldngDstrctNm" }, + { id: "causeDetailCode", title: "침수원인상세코드", widthPercent: 8, sortable: true, sortKey: "fldngCsDtlCd" }, + { id: "causeDetailName", title: "침수원인상세", widthPercent: 12, sortable: false }, + { id: "avgFloodDepthDisplay", title: "평균침수위", widthPercent: 7, sortable: true, sortKey: "avgFldngWtlv" }, + { id: "floodAreaDisplay", title: "침수면적", widthPercent: 7, sortable: true, sortKey: "fldngArea" }, + { id: "exmnYearDisplay", title: "조사년도", widthPercent: 6, sortable: true, sortKey: "exmnYr" }, + { id: "damageAmountDisplay", title: "피해금액", widthPercent: 9, sortable: true, sortKey: "damAmt" }, +]; + +export const FLOOD_HISTORY_DOWNLOAD_HEADERS: Record = { + rowNo: "NO", + floodYearDisplay: "침수년도", + ctpvNm: "시도", + sggNm: "시군구", + disasterName: "침수재해명", + rainBgngTmDisplay: "강우시작일시", + rainEndTmDisplay: "강우종료일시", + rainTotDisplay: "총강우량(mm)", + rainStrDisplay: "강우강도(mm/hr)", + floodHourDisplay: "침수시간", + districtName: "침수지구명", + causeDetailCode: "침수원인상세코드", + causeDetailName: "침수원인상세", + avgFloodDepthDisplay: "평균침수위", + floodAreaDisplay: "침수면적", + exmnYearDisplay: "조사년도", + damageAmountDisplay: "피해금액", +}; + +const toRadian = (degree: number) => (degree * Math.PI) / 180; + +const createIrregularRing = ({ + centerX, + centerY, + radiusX, + radiusY, + scales, + rotationDeg = 0, +}: { + centerX: number; + centerY: number; + radiusX: number; + radiusY: number; + scales: number[]; + rotationDeg?: number; +}): [number, number][] => { + const rotation = toRadian(rotationDeg); + const points = scales.map((scale, index) => { + const angle = rotation + (Math.PI * 2 * index) / scales.length; + const jitterX = radiusX * scale * Math.cos(angle); + const jitterY = radiusY * scale * Math.sin(angle); + return [centerX + jitterX, centerY + jitterY] as [number, number]; + }); + + return [...points, points[0]]; +}; + +const toNumericSeed = (value: string) => + Array.from(value).reduce((sum, char, index) => sum + char.charCodeAt(0) * (index + 1), 0); + +const buildGeometrySeed = (fid: string) => { + const seed = toNumericSeed(fid); + return { + centerX: 1050000 + (seed % 120000), + centerY: 1680000 + (seed % 150000), + width: 14000 + (seed % 9000), + height: 11000 + (seed % 7000), + }; +}; + +const createDepthZones = ( + historyId: string, + centerX: number, + centerY: number, +): FloodHistoryDepthZone[] => { + return [ + { + zoneId: `${historyId}-depth-1`, + depthLabel: "0.0~0.5m", + color: "#fff27a", + ring: createIrregularRing({ + centerX: centerX - 2600, + centerY: centerY - 1200, + radiusX: 2900, + radiusY: 2300, + rotationDeg: -18, + scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04], + }), + }, + { + zoneId: `${historyId}-depth-2`, + depthLabel: "0.5~1.0m", + color: "#c2f13d", + ring: createIrregularRing({ + centerX: centerX + 1800, + centerY: centerY - 600, + radiusX: 3500, + radiusY: 2800, + rotationDeg: 12, + scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95], + }), + }, + { + zoneId: `${historyId}-depth-3`, + depthLabel: "1.0~2.0m", + color: "#67d8ff", + ring: createIrregularRing({ + centerX, + centerY: centerY + 1800, + radiusX: 4700, + radiusY: 3900, + rotationDeg: 8, + scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84], + }), + }, + { + zoneId: `${historyId}-depth-4`, + depthLabel: "2.0~5.0m", + color: "#8f8bff", + ring: createIrregularRing({ + centerX: centerX + 3200, + centerY: centerY + 3200, + radiusX: 2600, + radiusY: 2000, + rotationDeg: 26, + scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9], + }), + }, + { + zoneId: `${historyId}-depth-5`, + depthLabel: "5.0m 이상", + color: "#f2528b", + ring: createIrregularRing({ + centerX: centerX + 400, + centerY: centerY - 3600, + radiusX: 1200, + radiusY: 1050, + rotationDeg: -10, + scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98], + }), + }, + ]; +}; + +const normalizeDate = (value?: string | null) => { + const source = String(value ?? "").trim(); + if (!/^\d{8}$/.test(source)) return ""; + return `${source.slice(0, 4)}-${source.slice(4, 6)}-${source.slice(6, 8)}`; +}; + +const normalizeHm = (value?: string | null) => { + const digits = String(value ?? "").replace(/[^0-9]/g, ""); + if (!digits) return ""; + if (digits.length === 1) return `0${digits}:00`; + if (digits.length === 2) return `${digits}:00`; + if (digits.length === 3) return `0${digits.slice(0, 1)}:${digits.slice(1, 3)}`; + return `${digits.slice(0, 2)}:${digits.slice(2, 4)}`; +}; + +const normalizeDateTime = (value?: string | null) => { + const source = String(value ?? "").trim(); + if (!source) return ""; + const date = new Date(source); + if (Number.isNaN(date.getTime())) return source.slice(0, 16).replace("T", " "); + const yyyy = date.getFullYear(); + const mm = `${date.getMonth() + 1}`.padStart(2, "0"); + const dd = `${date.getDate()}`.padStart(2, "0"); + const hh = `${date.getHours()}`.padStart(2, "0"); + const mi = `${date.getMinutes()}`.padStart(2, "0"); + return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; +}; + +const formatNumber = (value?: number | null, digits = 1, suffix = "") => { + const numeric = Number(value ?? 0); + if (!Number.isFinite(numeric)) return `0${suffix}`; + return `${numeric.toFixed(digits)}${suffix}`; +}; + +const formatInteger = (value?: number | null, suffix = "") => { + const numeric = Number(value ?? 0); + if (!Number.isFinite(numeric)) return `0${suffix}`; + return `${numeric.toLocaleString("ko-KR")}${suffix}`; +}; + +const buildCauseDetailName = (item: FloodHistoryApiItem) => { + const direct = String(item.dtlOcrnCsCn ?? "").trim(); + if (direct) return direct; + const code = String(item.fldngCsDtlCd ?? "").trim(); + return code ? `원인코드 ${code}` : "-"; +}; + +const buildGradeLabel = (avgFloodDepthM: number) => { + if (avgFloodDepthM >= 2) return "3등급"; + if (avgFloodDepthM >= 1) return "2등급"; + return "1등급"; +}; + +export const normalizeFloodHistoryFilters = ( + filters: FloodHistoryFilters, +): FloodHistoryFilters => ({ + ctpvCd: String(filters.ctpvCd ?? "0") || "0", + sggCd: String(filters.sggCd ?? "0") || "0", + floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4), + disasterName: String(filters.disasterName ?? "").trim(), +}); + +export const buildFloodHistoryYearOptions = () => { + const currentYear = new Date().getFullYear(); + return Array.from({ length: 30 }, (_, index) => String(currentYear - index)); +}; + +export const buildFloodHistoryListQueryString = ( + filters: FloodHistoryFilters, + page: number, + size: number, + sort?: UiTableSort, +) => { + const normalized = normalizeFloodHistoryFilters(filters); + const params = new URLSearchParams({ + page: String(page - 1), + size: String(size), + }); + + if (normalized.ctpvCd !== "0") params.set("ctpvCd", normalized.ctpvCd.slice(0, 2)); + if (normalized.sggCd !== "0") params.set("sggCd", normalized.sggCd.slice(0, 5)); + if (normalized.floodYear) params.set("fldngYr", normalized.floodYear); + if (normalized.disasterName) params.set("fldngDstrNm", normalized.disasterName); + if (sort?.key && sort.direction) params.set("sort", `${sort.key},${sort.direction}`); + + return params.toString(); +}; + +export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`; + +export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`; + +export const attachFloodHistoryGeometry = (item: FloodHistoryApiItem): FloodHistoryItem => { + const historyId = String(item.fid ?? ""); + const geometrySeed = buildGeometrySeed(historyId); + const startDate = normalizeDate(item.bgngYmd); + const endDate = normalizeDate(item.endYmd); + const startTime = normalizeHm(item.fldngBgngHm); + const endTime = normalizeHm(item.fldngEndHm); + const avgFloodDepthM = Number(item.avgFldngWtlv ?? 0); + const floodAreaHa = Number(item.fldngArea ?? 0); + + return { + ...item, + historyId, + ctpvNm: "", + sggNm: "", + floodYear: String(item.fldngYr ?? "").trim(), + exmnYear: String(item.exmnYr ?? "").trim(), + disasterName: String(item.fldngDstrNm ?? "").trim(), + districtName: String(item.fldngDstrctNm ?? "").trim(), + floodAreaHa, + avgFloodDepthM, + floodDepthM: avgFloodDepthM, + gradeLabel: buildGradeLabel(avgFloodDepthM), + startDate, + endDate, + startTime, + endTime, + floodHour: formatNumber(Number(item.fldngHr ?? 0), 1, "시간"), + causeDetailCode: String(item.fldngCsDtlCd ?? "").trim(), + causeDetailName: buildCauseDetailName(item), + damageAmount: formatInteger(Number(item.damAmt ?? 0), "원"), + rainStartDateTime: normalizeDateTime(item.rainBgngTm), + rainEndDateTime: normalizeDateTime(item.rainEndTm), + rainStartTime: normalizeDateTime(item.rainBgngTm).slice(11, 16), + rainEndTime: normalizeDateTime(item.rainEndTm).slice(11, 16), + totalRainfallMm: Number(item.rainTot ?? 0), + rainfallIntensityMmHr: Number(item.rainStr ?? 0), + areaRing: createIrregularRing({ + centerX: geometrySeed.centerX, + centerY: geometrySeed.centerY, + radiusX: geometrySeed.width / 2, + radiusY: geometrySeed.height / 2, + rotationDeg: 9, + scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86], + }), + depthZones: createDepthZones(historyId, geometrySeed.centerX, geometrySeed.centerY), + }; +}; + +export const toFloodHistoryTableRows = ( + items: FloodHistoryItem[], + page: number, + pageSize = FLOOD_HISTORY_PAGE_SIZE, +): FloodHistoryTableRow[] => + items.map((item, index) => ({ + ...item, + rowNo: (page - 1) * pageSize + index + 1, + floodYearDisplay: item.floodYear ? `${item.floodYear}년` : "-", + floodHourDisplay: item.floodHour || "-", + avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM), + floodAreaDisplay: formatFloodArea(item.floodAreaHa), + exmnYearDisplay: item.exmnYear ? `${item.exmnYear}년` : "-", + damageAmountDisplay: item.damageAmount || "-", + rainBgngTmDisplay: item.rainStartDateTime || "-", + rainEndTmDisplay: item.rainEndDateTime || "-", + rainTotDisplay: formatNumber(item.totalRainfallMm, 1), + rainStrDisplay: formatNumber(item.rainfallIntensityMmHr, 1), + })); + +const resolveFloodHistoryNodeName = (node: any) => + String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim(); + +const resolveFloodHistoryLayerType = (node: any) => + String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim(); + +export const isFloodHistoryNode = (node: any) => { + const name = resolveFloodHistoryNodeName(node); + const layerType = resolveFloodHistoryLayerType(node); + return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE; +}; diff --git a/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx deleted file mode 100644 index f9ae60995..000000000 --- a/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx +++ /dev/null @@ -1,408 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; -import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui"; -import UiPageNations from "@common/business_components/ui/table/uiPageNations"; -import useStore from "@/store/useStore"; -import { - buildFloodHistoryYearOptions, - buildFloodHistoryTableRows, - FLOOD_HISTORY_DEFAULT_FILTERS, - FLOOD_HISTORY_DOWNLOAD_HEADERS, - FLOOD_HISTORY_ITEMS, - FLOOD_HISTORY_PAGE_SIZE, - FLOOD_HISTORY_ROW_SELECT_MESSAGE, - formatFloodArea, - formatFloodDepth, - normalizeFloodHistoryFilters, - type FloodHistoryFilters, - type FloodHistoryItem, -} from "./floodHistoryTypes"; - -const resolveOptionText = ( - code: string, - options: Array<{ text: string; val: string | number }>, - prefixLength?: number, -) => { - const normalized = String(code ?? "").trim(); - if (!normalized || normalized === "0") return ""; - const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized; - return options.find((item) => String(item.val).trim() === lookup)?.text ?? ""; -}; - -const GRID_COLUMNS = [ - { id: "rowNo", title: "NO", width: "64px", align: "center" }, - { id: "ctpvNm", title: "시도", width: "90px", align: "center" }, - { id: "sggNm", title: "시군구", width: "100px", align: "center" }, - { id: "startDateDisplay", title: "침수시작일자", width: "118px", align: "center" }, - { id: "endDateDisplay", title: "침수종료일자", width: "118px", align: "center" }, - { id: "startTimeDisplay", title: "침수시작시간", width: "98px", align: "center" }, - { id: "endTimeDisplay", title: "침수종료시간", width: "98px", align: "center" }, - { id: "floodYearDisplay", title: "침수년도", width: "96px", align: "center" }, - { id: "floodHourDisplay", title: "침수시간", width: "100px", align: "center" }, - { id: "disasterName", title: "침수재해명", width: "190px", align: "left" }, - { id: "districtNameDisplay", title: "침수지구명", width: "150px", align: "left" }, - { id: "causeCodeDisplay", title: "침수원인상세코드", width: "132px", align: "center" }, - { id: "causeDetailDisplay", title: "침수원인상세", width: "190px", align: "left" }, - { id: "avgFloodDepthDisplay", title: "평균침수위", width: "104px", align: "right" }, - { id: "floodAreaDisplay", title: "침수면적", width: "104px", align: "right" }, - { id: "exmnYearDisplay", title: "조사년도", width: "96px", align: "center" }, - { id: "damageAmountDisplay", title: "피해금액", width: "132px", align: "right" }, -] as const; - -const buildDownloadRows = (rows: FloodHistoryItem[]) => - rows.map((row, index) => ({ - rowNo: index + 1, - ctpvNm: row.ctpvNm, - sggNm: row.sggNm, - disasterName: row.disasterName, - exmnYearDisplay: `${row.exmnYear}년`, - avgFloodDepthDisplay: formatFloodDepth(row.avgFloodDepthM), - floodAreaDisplay: formatFloodArea(row.floodAreaHa), - causeDetailDisplay: row.causeDetailName, - startDateDisplay: row.startDate, - endDateDisplay: row.endDate, - startTimeDisplay: row.startTime, - endTimeDisplay: row.endTime, - causeCodeDisplay: row.causeDetailCode, - districtNameDisplay: row.districtName, - damageAmountDisplay: row.damageAmount, - floodHourDisplay: row.floodHour, - floodYearDisplay: `${row.floodYear}년`, - })); - -export default function FloodHistoryPopupPage() { - const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); - // 조회조건 draft는 popup 내부에서 사용자가 편집 중인 값이다. - const [filters, setFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); - // 적용된 조회조건과 선택행은 다운로드/페이지 이동에도 같은 기준을 유지하기 위해 분리한다. - const [appliedFilters, setAppliedFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); - const [currentPage, setCurrentPage] = useState(1); - const [selectedHistoryId, setSelectedHistoryId] = useState(""); - - useEffect(() => { - if (ctpv.length === 0) void fetchCtpv(); - if (sgg.length === 0) void fetchSgg(); - }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); - - const floodYearOptions = useMemo( - () => buildFloodHistoryYearOptions(FLOOD_HISTORY_ITEMS), - [], - ); - - const sggOptions = useMemo( - () => - filters.ctpvCd === "0" - ? sgg - : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))), - [filters.ctpvCd, sgg], - ); - - const filteredRows = useMemo(() => { - const normalized = normalizeFloodHistoryFilters(appliedFilters); - const selectedCtpvName = resolveOptionText(normalized.ctpvCd, ctpv as any, 2); - const selectedSggName = resolveOptionText(normalized.sggCd, sgg as any, 5); - const disasterToken = normalized.disasterName.toLocaleLowerCase("ko-KR"); - - return FLOOD_HISTORY_ITEMS.filter((item) => { - if (selectedCtpvName && item.ctpvNm !== selectedCtpvName) return false; - if (selectedSggName && item.sggNm !== selectedSggName) return false; - if (normalized.floodYear && item.floodYear !== normalized.floodYear) return false; - if ( - disasterToken && - !item.disasterName.toLocaleLowerCase("ko-KR").includes(disasterToken) && - !item.districtName.toLocaleLowerCase("ko-KR").includes(disasterToken) - ) { - return false; - } - return true; - }).sort((left, right) => { - if (left.floodYear !== right.floodYear) { - return Number(right.floodYear) - Number(left.floodYear); - } - return left.disasterName.localeCompare(right.disasterName, "ko"); - }); - }, [appliedFilters, ctpv, sgg]); - - const pagedRows = useMemo(() => { - const start = (currentPage - 1) * FLOOD_HISTORY_PAGE_SIZE; - const end = start + FLOOD_HISTORY_PAGE_SIZE; - return buildFloodHistoryTableRows( - filteredRows.slice(start, end), - currentPage, - FLOOD_HISTORY_PAGE_SIZE, - ); - }, [currentPage, filteredRows]); - - const handleSearch = () => { - const normalized = normalizeFloodHistoryFilters(filters); - - if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") { - alert("시도를 먼저 선택하세요."); - return; - } - if (normalized.floodYear && normalized.floodYear.length !== 4) { - alert("침수발생년도는 4자리 연도로 입력하세요."); - return; - } - - setAppliedFilters(normalized); - setCurrentPage(1); - setSelectedHistoryId(""); - }; - - const handleDownload = async () => { - if (filteredRows.length === 0) { - alert("다운로드할 침수발생이력 데이터가 없습니다."); - return; - } - - await downloadExcelWithKoreanHeaders({ - data: buildDownloadRows(filteredRows) as unknown as JSON[], - fileName: `침수흔적도_${new Date().toISOString().slice(0, 10)}.xlsx`, - sheetNameArray: ["침수흔적도"], - headerRange: "A1:Q1", - headerMapping: FLOOD_HISTORY_DOWNLOAD_HEADERS, - }); - }; - - const handleRowDoubleClick = (row: FloodHistoryItem) => { - window.opener?.postMessage( - { - type: FLOOD_HISTORY_ROW_SELECT_MESSAGE, - row, - openDetail: false, - }, - window.location.origin, - ); - window.close(); - }; - - const handleRowClick = (row: FloodHistoryItem) => { - setSelectedHistoryId(row.historyId); - // popup을 유지한 채 메인 지도에 mock 침수흔적/침수심 preview를 먼저 보여준다. - window.opener?.postMessage( - { - type: FLOOD_HISTORY_ROW_SELECT_MESSAGE, - row, - openDetail: false, - }, - window.location.origin, - ); - }; - - return ( -
-
-

침수흔적도 목록

-
-
- -
-
-
- - - - - - - - -
- - -
-
-
- -
-
-

- 침수흔적도 조회 결과 {filteredRows.length.toLocaleString("ko-KR")}건 -

-
- -
- - - {GRID_COLUMNS.map((head) => ( - - ))} - - - - NO - 시도 - 시군구 - 강우 분석정보 - 침수재해명 - 침수년도 - 침수시간 - 침수지구명 - 침수원인상세코드 - 침수원인상세 - 평균침수위 - 침수면적 - 조사년도 - 피해금액 - - - 침수시작일자 - 침수종료일자 - 침수시작시간 - 침수종료시간 - - - - {pagedRows.length > 0 ? ( - pagedRows.map((row) => { - const isSelected = selectedHistoryId === row.historyId; - - return ( - handleRowClick(row)} - onDoubleClick={() => handleRowDoubleClick(row)} - > - {row.rowNo} - {row.ctpvNm} - {row.sggNm} - {row.startDateDisplay} - {row.endDateDisplay} - {row.startTimeDisplay} - {row.endTimeDisplay} - {row.disasterName} - {row.floodYearDisplay} - {row.floodHourDisplay} - {row.districtNameDisplay} - {row.causeCodeDisplay} - {row.causeDetailDisplay} - {row.avgFloodDepthDisplay} - {row.floodAreaDisplay} - {row.exmnYearDisplay} - {row.damageAmountDisplay} - - ); - }) - ) : ( - - - 조회된 데이터가 없습니다. - - - )} - -
-
- - {filteredRows.length > 0 ? ( - - ) : null} - -
- 행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다. -
-
-
-
- ); -} diff --git a/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts b/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts deleted file mode 100644 index fd6ef484e..000000000 --- a/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts +++ /dev/null @@ -1,458 +0,0 @@ -"use client"; - -export const FLOOD_HISTORY_POPUP_KEY = "floodHistoryPopup"; -export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; -export const FLOOD_HISTORY_LAYER_TYPE = "tb_sn_flood_trace_mock"; -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 FLOOD_HISTORY_PAGE_SIZE = 10; - -export type FloodHistoryFilters = { - ctpvCd: string; - sggCd: string; - floodYear: string; - disasterName: string; -}; - -export type FloodHistoryDepthZone = { - zoneId: string; - depthLabel: string; - color: string; - ring: [number, number][]; -}; - -export type FloodHistoryItem = { - historyId: string; - ctpvNm: string; - sggNm: string; - floodYear: string; - exmnYear: string; - disasterName: string; - districtName: string; - floodAreaHa: number; - avgFloodDepthM: number; - floodDepthM: number; - gradeLabel: string; - startDate: string; - endDate: string; - startTime: string; - endTime: string; - floodHour: string; - causeDetailCode: string; - causeDetailName: string; - damageAmount: string; - rainStartTime: string; - rainEndTime: string; - totalRainfallMm: number; - rainfallIntensityMmHr: number; - areaRing: [number, number][]; - depthZones: FloodHistoryDepthZone[]; -}; - -export type FloodHistoryTableRow = FloodHistoryItem & { - rowNo: number; - exmnYearDisplay: string; - avgFloodDepthDisplay: string; - floodAreaDisplay: string; - causeDetailDisplay: string; - startDateDisplay: string; - endDateDisplay: string; - startTimeDisplay: string; - endTimeDisplay: string; - causeCodeDisplay: string; - districtNameDisplay: string; - damageAmountDisplay: string; - floodHourDisplay: string; - floodYearDisplay: string; -}; - -export const FLOOD_HISTORY_DEFAULT_FILTERS: FloodHistoryFilters = { - ctpvCd: "0", - sggCd: "0", - floodYear: "", - disasterName: "", -}; - -export const FLOOD_HISTORY_DOWNLOAD_HEADERS: Record = { - rowNo: "NO", - ctpvNm: "시도", - sggNm: "시군구", - disasterName: "침수재해명", - exmnYearDisplay: "조사년도", - avgFloodDepthDisplay: "평균침수위", - floodAreaDisplay: "침수면적", - causeDetailDisplay: "침수원인상세", - startDateDisplay: "침수시작일자", - endDateDisplay: "침수종료일자", - startTimeDisplay: "침수시작시간", - endTimeDisplay: "침수종료시간", - causeCodeDisplay: "침수원인상세코드", - districtNameDisplay: "침수지구명", - damageAmountDisplay: "피해금액", - floodHourDisplay: "침수시간", - floodYearDisplay: "침수년도", -}; - -export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [ - { color: "#fff27a", label: "0.0~0.5m" }, - { color: "#c2f13d", label: "0.5~1.0m" }, - { color: "#67d8ff", label: "1.0~2.0m" }, - { color: "#8f8bff", label: "2.0~5.0m" }, - { color: "#f2528b", label: "5.0m 이상" }, -] as const; - -const toRadian = (degree: number) => (degree * Math.PI) / 180; - -const createIrregularRing = ({ - centerX, - centerY, - radiusX, - radiusY, - scales, - rotationDeg = 0, -}: { - centerX: number; - centerY: number; - radiusX: number; - radiusY: number; - scales: number[]; - rotationDeg?: number; -}): [number, number][] => { - const rotation = toRadian(rotationDeg); - const points = scales.map((scale, index) => { - const angle = rotation + (Math.PI * 2 * index) / scales.length; - const jitterX = radiusX * scale * Math.cos(angle); - const jitterY = radiusY * scale * Math.sin(angle); - return [centerX + jitterX, centerY + jitterY] as [number, number]; - }); - - return [...points, points[0]]; -}; - -const createDepthZones = ( - historyId: string, - centerX: number, - centerY: number, -): FloodHistoryDepthZone[] => { - return [ - { - zoneId: `${historyId}-depth-1`, - depthLabel: "0.0~0.5m", - color: "#fff27a", - ring: createIrregularRing({ - centerX: centerX - 5200, - centerY: centerY - 2600, - radiusX: 3600, - radiusY: 2900, - rotationDeg: -18, - scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04], - }), - }, - { - zoneId: `${historyId}-depth-2`, - depthLabel: "0.5~1.0m", - color: "#c2f13d", - ring: createIrregularRing({ - centerX: centerX + 2600, - centerY: centerY - 1200, - radiusX: 4300, - radiusY: 3600, - rotationDeg: 12, - scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95], - }), - }, - { - zoneId: `${historyId}-depth-3`, - depthLabel: "1.0~2.0m", - color: "#67d8ff", - ring: createIrregularRing({ - centerX: centerX - 600, - centerY: centerY + 2600, - radiusX: 5600, - radiusY: 4700, - rotationDeg: 8, - scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84], - }), - }, - { - zoneId: `${historyId}-depth-4`, - depthLabel: "2.0~5.0m", - color: "#8f8bff", - ring: createIrregularRing({ - centerX: centerX + 4200, - centerY: centerY + 5200, - radiusX: 3200, - radiusY: 2500, - rotationDeg: 26, - scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9], - }), - }, - { - zoneId: `${historyId}-depth-5`, - depthLabel: "5.0m 이상", - color: "#f2528b", - ring: createIrregularRing({ - centerX: centerX + 600, - centerY: centerY - 5200, - radiusX: 1500, - radiusY: 1350, - rotationDeg: -10, - scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98], - }), - }, - ]; -}; - -const createHistory = ( - item: Omit & { - centerX: number; - centerY: number; - width: number; - height: number; - }, -): FloodHistoryItem => { - const { centerX, centerY, width, height, ...detail } = item; - - return { - ...detail, - areaRing: createIrregularRing({ - centerX, - centerY, - radiusX: width / 2, - radiusY: height / 2, - rotationDeg: 9, - scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86], - }), - depthZones: createDepthZones(detail.historyId, centerX, centerY), - }; -}; - -export const FLOOD_HISTORY_ITEMS: FloodHistoryItem[] = [ - createHistory({ - historyId: "FH-2024-001", - ctpvNm: "부산광역시", - sggNm: "중구", - floodYear: "2024", - exmnYear: "2025", - disasterName: "중앙동 저지대 침수", - districtName: "중앙동 저지대", - floodAreaHa: 1.8, - avgFloodDepthM: 0.62, - floodDepthM: 0.62, - gradeLabel: "1등급", - startDate: "2024-07-18", - endDate: "2024-07-18", - startTime: "14:10", - endTime: "18:20", - floodHour: "4.2시간", - causeDetailCode: "FC-01", - causeDetailName: "집중강우", - damageAmount: "120,000,000원", - rainStartTime: "13:50", - rainEndTime: "18:20", - totalRainfallMm: 91, - rainfallIntensityMmHr: 31, - centerX: 1122000, - centerY: 1705000, - width: 18000, - height: 14000, - }), - createHistory({ - historyId: "FH-2023-002", - ctpvNm: "경상남도", - sggNm: "창원시", - floodYear: "2023", - exmnYear: "2024", - disasterName: "팔용동 공단 배수지연 침수", - districtName: "팔용동 공단", - floodAreaHa: 4.8, - avgFloodDepthM: 1.92, - floodDepthM: 1.92, - gradeLabel: "2등급", - startDate: "2023-08-11", - endDate: "2023-08-11", - startTime: "09:40", - endTime: "15:10", - floodHour: "5.5시간", - causeDetailCode: "FC-04", - causeDetailName: "배수시설 처리능력 초과", - damageAmount: "340,000,000원", - rainStartTime: "08:55", - rainEndTime: "15:05", - totalRainfallMm: 138, - rainfallIntensityMmHr: 42, - centerX: 1081000, - centerY: 1778000, - width: 25000, - height: 17000, - }), - createHistory({ - historyId: "FH-2022-003", - ctpvNm: "울산광역시", - sggNm: "남구", - floodYear: "2022", - exmnYear: "2023", - disasterName: "태화강 제내지 도로 침수", - districtName: "여천동 하류부", - floodAreaHa: 3.1, - avgFloodDepthM: 1.15, - floodDepthM: 1.15, - gradeLabel: "2등급", - startDate: "2022-09-06", - endDate: "2022-09-06", - startTime: "07:20", - endTime: "12:40", - floodHour: "5.3시간", - causeDetailCode: "FC-02", - causeDetailName: "하천 수위 상승", - damageAmount: "210,000,000원", - rainStartTime: "06:30", - rainEndTime: "11:50", - totalRainfallMm: 117, - rainfallIntensityMmHr: 37, - centerX: 1168000, - centerY: 1811000, - width: 22000, - height: 16000, - }), - createHistory({ - historyId: "FH-2021-004", - ctpvNm: "부산광역시", - sggNm: "서구", - floodYear: "2021", - exmnYear: "2022", - disasterName: "서대신동 급경사지 유출 침수", - districtName: "서대신동 일원", - floodAreaHa: 2.4, - avgFloodDepthM: 0.85, - floodDepthM: 0.85, - gradeLabel: "1등급", - startDate: "2021-07-07", - endDate: "2021-07-07", - startTime: "16:00", - endTime: "20:10", - floodHour: "4.1시간", - causeDetailCode: "FC-03", - causeDetailName: "우수관로 월류", - damageAmount: "175,000,000원", - rainStartTime: "15:20", - rainEndTime: "19:50", - totalRainfallMm: 104, - rainfallIntensityMmHr: 29, - centerX: 1111000, - centerY: 1709000, - width: 20000, - height: 14500, - }), - createHistory({ - historyId: "FH-2020-005", - ctpvNm: "부산광역시", - sggNm: "영도구", - floodYear: "2020", - exmnYear: "2021", - disasterName: "청학동 해안도로 월파 침수", - districtName: "청학동 해안부", - floodAreaHa: 1.2, - avgFloodDepthM: 0.48, - floodDepthM: 0.48, - gradeLabel: "1등급", - startDate: "2020-10-22", - endDate: "2020-10-22", - startTime: "03:10", - endTime: "06:30", - floodHour: "3.3시간", - causeDetailCode: "FC-06", - causeDetailName: "해안 월파", - damageAmount: "82,000,000원", - rainStartTime: "02:40", - rainEndTime: "05:50", - totalRainfallMm: 55, - rainfallIntensityMmHr: 18, - centerX: 1129000, - centerY: 1695000, - width: 15000, - height: 12000, - }), - createHistory({ - historyId: "FH-2019-006", - ctpvNm: "경상남도", - sggNm: "김해시", - floodYear: "2019", - exmnYear: "2020", - disasterName: "장유 배수분구 내수 침수", - districtName: "장유 배수분구", - floodAreaHa: 2.9, - avgFloodDepthM: 0.96, - floodDepthM: 0.96, - gradeLabel: "1등급", - startDate: "2019-09-21", - endDate: "2019-09-21", - startTime: "11:30", - endTime: "16:15", - floodHour: "4.8시간", - causeDetailCode: "FC-05", - causeDetailName: "배수펌프 지연 가동", - damageAmount: "143,000,000원", - rainStartTime: "10:45", - rainEndTime: "15:50", - totalRainfallMm: 88, - rainfallIntensityMmHr: 24, - centerX: 1098000, - centerY: 1734000, - width: 21000, - height: 15000, - }), -]; - -export const normalizeFloodHistoryFilters = ( - filters: FloodHistoryFilters, -): FloodHistoryFilters => ({ - ctpvCd: String(filters.ctpvCd ?? "0") || "0", - sggCd: String(filters.sggCd ?? "0") || "0", - floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4), - disasterName: String(filters.disasterName ?? "").trim(), -}); - -export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`; - -export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`; - -export const buildFloodHistoryTableRows = ( - items: FloodHistoryItem[], - page: number, - pageSize = FLOOD_HISTORY_PAGE_SIZE, -): FloodHistoryTableRow[] => - items.map((item, index) => ({ - ...item, - rowNo: (page - 1) * pageSize + index + 1, - exmnYearDisplay: `${item.exmnYear}년`, - avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM), - floodAreaDisplay: formatFloodArea(item.floodAreaHa), - causeDetailDisplay: item.causeDetailName, - startDateDisplay: item.startDate, - endDateDisplay: item.endDate, - startTimeDisplay: item.startTime, - endTimeDisplay: item.endTime, - causeCodeDisplay: item.causeDetailCode, - districtNameDisplay: item.districtName, - damageAmountDisplay: item.damageAmount, - floodHourDisplay: item.floodHour, - floodYearDisplay: `${item.floodYear}년`, - })); - -export const buildFloodHistoryYearOptions = (items: FloodHistoryItem[]) => - Array.from(new Set(items.map((item) => item.floodYear))) - .sort((left, right) => Number(right) - Number(left)); - -const resolveFloodHistoryNodeName = (node: any) => - String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim(); - -const resolveFloodHistoryLayerType = (node: any) => - String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim(); - -export const isFloodHistoryNode = (node: any) => { - const name = resolveFloodHistoryNodeName(node); - const layerType = resolveFloodHistoryLayerType(node); - return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE; -}; diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 853fe2b1d..32297e76b 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -83,7 +83,7 @@ import { useInftwtResultMap } from "./components/inftwtResult/useInftwtResultMap import { useRdiiResultMap } from "./components/rdiiResult/useRdiiResultMap"; import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel"; import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel"; -import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel"; +import FldngHstryDetailPanel from "./components/fldngHstry/FldngHstryDetailPanel"; import { buildSinkholeDefaultYearRange, buildSinkholeFiltersFromYearRange, @@ -118,7 +118,7 @@ import { FLOOD_HISTORY_POPUP_KEY, isFloodHistoryNode, type FloodHistoryItem, -} from "./components/floodHistory/floodHistoryTypes"; +} from "./components/fldngHstry/fldngHstryTypes"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; import SearchModal from "./searchModal/searchModal"; import { WATER_DEPTH_LAYER_TYPES, extractWaterDepthLegendItems } from "./waterDepth/waterDepth"; @@ -7288,7 +7288,7 @@ const Map = () => { ) : null} {selectedFloodHistoryDetail && floodHistoryDetailPopupPosition ? ( - ; \ No newline at end of file +} as const satisfies Record; diff --git a/apps/sn/src/features/windowPopup/ui/FldngHstryPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/FldngHstryPopupContent.tsx new file mode 100644 index 000000000..fcc8c0a90 --- /dev/null +++ b/apps/sn/src/features/windowPopup/ui/FldngHstryPopupContent.tsx @@ -0,0 +1,5 @@ +import FldngHstryPopupPage from "@/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage"; + +export default function FldngHstryPopupContent() { + return ; +} diff --git a/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx deleted file mode 100644 index d363c7ecf..000000000 --- a/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import FloodHistoryPopupPage from "@/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage"; - -export default function FloodHistoryPopupContent() { - return ; -} From c13cab394aa3e3d712512cd393ce318b8a384c5d Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 24 Jul 2026 15:23:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fldngHstry/FldngHstryDetailPanel.tsx | 4 +- .../fldngHstry/FldngHstryPopupPage.tsx | 85 +++++----- .../components/fldngHstry/fldngHstryTypes.ts | 73 +++++---- apps/sn/app/(map-layout)/map/page.tsx | 148 +++++++++--------- 4 files changed, 159 insertions(+), 151 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx index a56aa7127..86160149c 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryDetailPanel.tsx @@ -1,10 +1,10 @@ "use client"; import { useMemo, useState } from "react"; -import { formatFloodArea, formatFloodDepth, type FloodHistoryItem } from "./fldngHstryTypes"; +import { formatFloodArea, formatFloodDepth, type FldngHstryItem } from "./fldngHstryTypes"; type Props = { - detail: FloodHistoryItem; + detail: FldngHstryItem; onClose: () => void; position: { left: number; 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 147fafdb2..470a7d3c2 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -1,3 +1,6 @@ +/** + * 침수발생이력 popup + */ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -8,20 +11,20 @@ import { getRawJson } from "@/components/lib/api"; import useStore from "@/store/useStore"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { - attachFloodHistoryGeometry, - buildFloodHistoryListQueryString, - buildFloodHistoryYearOptions, - FLOOD_HISTORY_DEFAULT_FILTERS, - FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE, - FLOOD_HISTORY_DOWNLOAD_HEADERS, - FLOOD_HISTORY_LIST_HEADLIST, - FLOOD_HISTORY_PAGE_SIZE, - FLOOD_HISTORY_ROW_SELECT_MESSAGE, - normalizeFloodHistoryFilters, - toFloodHistoryTableRows, - type FloodHistoryApiItem, - type FloodHistoryFilters, - type FloodHistoryTableRow, + FLDNG_HSTRY_DEFAULT_FILTERS, + FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE, + FLDNG_HSTRY_DOWNLOAD_HEADERS, + FLDNG_HSTRY_LIST_HEADLIST, + FLDNG_HSTRY_PAGE_SIZE, + FLDNG_HSTRY_ROW_SELECT_MESSAGE, + attachFldngHstryGeometry, + buildFldngHstryListQueryString, + buildFldngHstryYearOptions, + normalizeFldngHstryFilters, + toFldngHstryTableRows, + type FldngHstryApiItem, + type FldngHstryFilters, + type FldngHstryTableRow, } from "./fldngHstryTypes"; const unwrapPayload = (response: any): T => @@ -38,12 +41,12 @@ const resolveOptionText = ( return options.find((item) => String(item.val).trim() === lookup)?.text ?? ""; }; -const resolveFloodHistoryApiSort = (sort?: UiTableSort) => { +const resolveFldngHstryApiSort = (sort?: UiTableSort) => { if (!sort?.key || !sort.direction) return undefined; return { key: sort.key, direction: sort.direction } as UiTableSort; }; -const buildDownloadRows = (rows: FloodHistoryTableRow[]) => +const buildDownloadRows = (rows: FldngHstryTableRow[]) => rows.map((row) => ({ rowNo: row.rowNo, floodYearDisplay: row.floodYearDisplay, @@ -65,10 +68,10 @@ const buildDownloadRows = (rows: FloodHistoryTableRow[]) => })); const resolveRegionNames = ( - row: FloodHistoryTableRow, + row: FldngHstryTableRow, ctpvOptions: Array<{ text: string; val: string | number }>, sggOptions: Array<{ text: string; val: string | number }>, -): FloodHistoryTableRow => ({ +): FldngHstryTableRow => ({ ...row, ctpvNm: resolveOptionText(row.ctpvCd ?? "", ctpvOptions, 2) || row.ctpvNm, sggNm: resolveOptionText(row.sggCd ?? "", sggOptions, 5) || row.sggNm, @@ -76,9 +79,9 @@ const resolveRegionNames = ( export default function FldngHstryPopupPage() { const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); - const [filters, setFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); - const [appliedFilters, setAppliedFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS); - const [rows, setRows] = useState([]); + const [filters, setFilters] = useState(FLDNG_HSTRY_DEFAULT_FILTERS); + const [appliedFilters, setAppliedFilters] = useState(FLDNG_HSTRY_DEFAULT_FILTERS); + const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [selectedIndex, setSelectedIndex] = useState(-1); @@ -100,7 +103,7 @@ export default function FldngHstryPopupPage() { ); }, [ctpv, rows.length, sgg]); - const floodYearOptions = useMemo(() => buildFloodHistoryYearOptions(), []); + const fldngYearOptions = useMemo(() => buildFldngHstryYearOptions(), []); const sggOptions = useMemo( () => @@ -111,20 +114,20 @@ export default function FldngHstryPopupPage() { ); const fetchRows = useCallback( - async (page: number, nextFilters: FloodHistoryFilters, size: number, nextSort?: UiTableSort) => { - const normalized = normalizeFloodHistoryFilters(nextFilters); - const queryString = buildFloodHistoryListQueryString( + async (page: number, nextFilters: FldngHstryFilters, size: number, nextSort?: UiTableSort) => { + const normalized = normalizeFldngHstryFilters(nextFilters); + const queryString = buildFldngHstryListQueryString( normalized, page, size, - resolveFloodHistoryApiSort(nextSort), + resolveFldngHstryApiSort(nextSort), ); const response = await getRawJson(`/sewerage-service/api/v1/fldng/hstry?${queryString}`); - const payload = unwrapPayload<{ items?: FloodHistoryApiItem[]; total?: number }>(response); + const payload = unwrapPayload<{ items?: FldngHstryApiItem[]; total?: number }>(response); const items = Array.isArray(payload?.items) ? payload.items : []; const displayItems = items.map((item) => { - const hydrated = attachFloodHistoryGeometry(item); + const hydrated = attachFldngHstryGeometry(item); return { ...hydrated, ctpvNm: resolveOptionText(hydrated.ctpvCd ?? "", ctpv as any, 2), @@ -133,7 +136,7 @@ export default function FldngHstryPopupPage() { }); return { - rows: toFloodHistoryTableRows(displayItems, page, size), + rows: toFldngHstryTableRows(displayItems, page, size), total: Number(payload?.total ?? 0), }; }, @@ -141,11 +144,11 @@ export default function FldngHstryPopupPage() { ); const loadRows = useCallback( - async (page: number, nextFilters: FloodHistoryFilters, nextSort?: UiTableSort) => { + async (page: number, nextFilters: FldngHstryFilters, nextSort?: UiTableSort) => { setIsLoading(true); try { - const normalized = normalizeFloodHistoryFilters(nextFilters); - const response = await fetchRows(page, normalized, FLOOD_HISTORY_PAGE_SIZE, nextSort); + const normalized = normalizeFldngHstryFilters(nextFilters); + const response = await fetchRows(page, normalized, FLDNG_HSTRY_PAGE_SIZE, nextSort); setRows(response.rows); setTotal(response.total); setCurrentPage(page); @@ -166,13 +169,13 @@ export default function FldngHstryPopupPage() { ); useEffect(() => { - void loadRows(1, FLOOD_HISTORY_DEFAULT_FILTERS, undefined); + void loadRows(1, FLDNG_HSTRY_DEFAULT_FILTERS, undefined); // 최초 로딩은 한 번만 수행한다. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSearch = async (page = 1) => { - const normalized = normalizeFloodHistoryFilters(filters); + const normalized = normalizeFldngHstryFilters(filters); if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") { alert("시도를 먼저 선택하세요."); @@ -199,7 +202,7 @@ export default function FldngHstryPopupPage() { setSelectedIndex(index); window.opener?.postMessage( { - type: FLOOD_HISTORY_ROW_SELECT_MESSAGE, + type: FLDNG_HSTRY_ROW_SELECT_MESSAGE, row, openDetail: false, }, @@ -218,7 +221,7 @@ export default function FldngHstryPopupPage() { const response = await fetchRows( 1, appliedFilters, - Math.max(total, FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE), + Math.max(total, FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE), appliedSort, ); await downloadExcelWithKoreanHeaders({ @@ -226,7 +229,7 @@ export default function FldngHstryPopupPage() { fileName: `침수발생이력_${new Date().toISOString().slice(0, 10)}.xlsx`, sheetNameArray: ["침수발생이력"], headerRange: "A1:Q1", - headerMapping: FLOOD_HISTORY_DOWNLOAD_HEADERS, + headerMapping: FLDNG_HSTRY_DOWNLOAD_HEADERS, }); } catch (error) { console.error("침수발생이력 다운로드 중 오류:", error); @@ -302,7 +305,7 @@ export default function FldngHstryPopupPage() { } > - {floodYearOptions.map((year) => ( + {fldngYearOptions.map((year) => ( @@ -352,11 +355,11 @@ export default function FldngHstryPopupPage() {
- + headName={<>침수발생이력 조회 결과 총 {total.toLocaleString()}건} tableData={rows} - headlist={FLOOD_HISTORY_LIST_HEADLIST} - pageSize={FLOOD_HISTORY_PAGE_SIZE} + headlist={FLDNG_HSTRY_LIST_HEADLIST} + pageSize={FLDNG_HSTRY_PAGE_SIZE} total={total} cellClick={(index) => handleRowClick(index)} onPageChange={(page) => void handlePageChange(page)} 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 9812c2937..94da67e29 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -3,29 +3,29 @@ import type { UiHeadProps, UiTableSort } from "@common/business_components/ui"; export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup"; -export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; +export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; export const FLOOD_HISTORY_LAYER_TYPE = "tb_sn_flood_trace_mock"; 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 FLOOD_HISTORY_PAGE_SIZE = 10; -export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE = 2000; +export const FLDNG_HSTRY_PAGE_SIZE = 10; +export const FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE = 2000; -export type FloodHistoryFilters = { +export type FldngHstryFilters = { ctpvCd: string; sggCd: string; floodYear: string; disasterName: string; }; -export type FloodHistoryDepthZone = { +export type FldngHstryDepthZone = { zoneId: string; depthLabel: string; color: string; ring: [number, number][]; }; -export type FloodHistoryApiItem = { +export type FldngHstryApiItem = { fid: string; fldngMapFid?: string; fldngWtlvMapSn?: number | null; @@ -58,7 +58,7 @@ export type FloodHistoryApiItem = { rainStr?: number | null; }; -export type FloodHistoryItem = FloodHistoryApiItem & { +export type FldngHstryItem = FldngHstryApiItem & { historyId: string; ctpvNm: string; sggNm: string; @@ -85,10 +85,10 @@ export type FloodHistoryItem = FloodHistoryApiItem & { totalRainfallMm: number; rainfallIntensityMmHr: number; areaRing: [number, number][]; - depthZones: FloodHistoryDepthZone[]; + depthZones: FldngHstryDepthZone[]; }; -export type FloodHistoryTableRow = FloodHistoryItem & { +export type FldngHstryTableRow = FldngHstryItem & { rowNo: number; floodYearDisplay: string; floodHourDisplay: string; @@ -102,14 +102,14 @@ export type FloodHistoryTableRow = FloodHistoryItem & { rainStrDisplay: string; }; -export const FLOOD_HISTORY_DEFAULT_FILTERS: FloodHistoryFilters = { +export const FLDNG_HSTRY_DEFAULT_FILTERS: FldngHstryFilters = { ctpvCd: "0", sggCd: "0", floodYear: "", disasterName: "", }; -export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [ +export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [ { color: "#fff27a", label: "0.0~0.5m" }, { color: "#c2f13d", label: "0.5~1.0m" }, { color: "#67d8ff", label: "1.0~2.0m" }, @@ -117,7 +117,7 @@ export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [ { color: "#f2528b", label: "5.0m 이상" }, ] as const; -export const FLOOD_HISTORY_LIST_HEADLIST: UiHeadProps[] = [ +export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [ { id: "rowNo", title: "NO", widthPercent: 5, sortable: false }, { id: "floodYearDisplay", title: "침수년도", widthPercent: 6, sortable: true, sortKey: "fldngYr" }, { id: "ctpvNm", title: "시도", widthPercent: 7, sortable: true, sortKey: "ctpvCd" }, @@ -137,7 +137,7 @@ export const FLOOD_HISTORY_LIST_HEADLIST: UiHeadProps[] = [ { id: "damageAmountDisplay", title: "피해금액", widthPercent: 9, sortable: true, sortKey: "damAmt" }, ]; -export const FLOOD_HISTORY_DOWNLOAD_HEADERS: Record = { +export const FLDNG_HSTRY_DOWNLOAD_HEADERS: Record = { rowNo: "NO", floodYearDisplay: "침수년도", ctpvNm: "시도", @@ -198,11 +198,11 @@ const buildGeometrySeed = (fid: string) => { }; }; -const createDepthZones = ( +const createFldngHstryDepthZones = ( historyId: string, centerX: number, centerY: number, -): FloodHistoryDepthZone[] => { +): FldngHstryDepthZone[] => { return [ { zoneId: `${historyId}-depth-1`, @@ -312,7 +312,7 @@ const formatInteger = (value?: number | null, suffix = "") => { return `${numeric.toLocaleString("ko-KR")}${suffix}`; }; -const buildCauseDetailName = (item: FloodHistoryApiItem) => { +const buildCauseDetailName = (item: FldngHstryApiItem) => { const direct = String(item.dtlOcrnCsCn ?? "").trim(); if (direct) return direct; const code = String(item.fldngCsDtlCd ?? "").trim(); @@ -325,27 +325,27 @@ const buildGradeLabel = (avgFloodDepthM: number) => { return "1등급"; }; -export const normalizeFloodHistoryFilters = ( - filters: FloodHistoryFilters, -): FloodHistoryFilters => ({ +export const normalizeFldngHstryFilters = ( + filters: FldngHstryFilters, +): FldngHstryFilters => ({ ctpvCd: String(filters.ctpvCd ?? "0") || "0", sggCd: String(filters.sggCd ?? "0") || "0", floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4), disasterName: String(filters.disasterName ?? "").trim(), }); -export const buildFloodHistoryYearOptions = () => { +export const buildFldngHstryYearOptions = () => { const currentYear = new Date().getFullYear(); return Array.from({ length: 30 }, (_, index) => String(currentYear - index)); }; -export const buildFloodHistoryListQueryString = ( - filters: FloodHistoryFilters, +export const buildFldngHstryListQueryString = ( + filters: FldngHstryFilters, page: number, size: number, sort?: UiTableSort, ) => { - const normalized = normalizeFloodHistoryFilters(filters); + const normalized = normalizeFldngHstryFilters(filters); const params = new URLSearchParams({ page: String(page - 1), size: String(size), @@ -364,7 +364,7 @@ export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`; export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`; -export const attachFloodHistoryGeometry = (item: FloodHistoryApiItem): FloodHistoryItem => { +export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryItem => { const historyId = String(item.fid ?? ""); const geometrySeed = buildGeometrySeed(historyId); const startDate = normalizeDate(item.bgngYmd); @@ -409,15 +409,15 @@ export const attachFloodHistoryGeometry = (item: FloodHistoryApiItem): FloodHist rotationDeg: 9, scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86], }), - depthZones: createDepthZones(historyId, geometrySeed.centerX, geometrySeed.centerY), + depthZones: createFldngHstryDepthZones(historyId, geometrySeed.centerX, geometrySeed.centerY), }; }; -export const toFloodHistoryTableRows = ( - items: FloodHistoryItem[], +export const toFldngHstryTableRows = ( + items: FldngHstryItem[], page: number, - pageSize = FLOOD_HISTORY_PAGE_SIZE, -): FloodHistoryTableRow[] => + pageSize = FLDNG_HSTRY_PAGE_SIZE, +): FldngHstryTableRow[] => items.map((item, index) => ({ ...item, rowNo: (page - 1) * pageSize + index + 1, @@ -433,14 +433,19 @@ export const toFloodHistoryTableRows = ( rainStrDisplay: formatNumber(item.rainfallIntensityMmHr, 1), })); -const resolveFloodHistoryNodeName = (node: any) => +const resolveFldngHstryNodeName = (node: any) => String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim(); -const resolveFloodHistoryLayerType = (node: any) => +const resolveFldngHstryLayerType = (node: any) => String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim(); -export const isFloodHistoryNode = (node: any) => { - const name = resolveFloodHistoryNodeName(node); - const layerType = resolveFloodHistoryLayerType(node); +export const isFldngHstryNode = (node: any) => { + const name = resolveFldngHstryNodeName(node); + const layerType = resolveFldngHstryLayerType(node); return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE; }; + +export const isFloodHistoryNode = isFldngHstryNode; +export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = FLDNG_HSTRY_ROW_SELECT_MESSAGE; +export const FLOOD_HISTORY_PAGE_SIZE = FLDNG_HSTRY_PAGE_SIZE; +export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE = FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE; diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 32297e76b..83e99f778 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -114,10 +114,10 @@ import { FLOOD_HISTORY_AREA_LAYER_ID, FLOOD_HISTORY_DEPTH_LAYER_ID, FLOOD_HISTORY_LAYER_TYPE, - FLOOD_HISTORY_ROW_SELECT_MESSAGE, FLOOD_HISTORY_POPUP_KEY, - isFloodHistoryNode, - type FloodHistoryItem, + FLDNG_HSTRY_ROW_SELECT_MESSAGE, + isFldngHstryNode, + type FldngHstryItem, } from "./components/fldngHstry/fldngHstryTypes"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; import SearchModal from "./searchModal/searchModal"; @@ -973,7 +973,7 @@ const getRightPanelFeatureDisplayLabel = (data: any, category: RightPanelTabCate return getRightPanelFeatureTitle(data); }; -const createFloodHistoryAreaStyle = (label: string) => +const createFldngHstryAreaStyle = (label: string) => new Style({ fill: new Fill({ color: "rgba(30, 136, 229, 0.16)", @@ -993,7 +993,7 @@ const createFloodHistoryAreaStyle = (label: string) => zIndex: 9994, }); -const createFloodHistoryDepthStyle = (label: string, color: string) => +const createFldngHstryDepthStyle = (label: string, color: string) => new Style({ fill: new Fill({ color, @@ -2544,7 +2544,7 @@ const Map = () => { const [checkedLayerLegendVersion, setCheckedLayerLegendVersion] = useState(0); const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false); const [isSinkholeChecked, setIsSinkholeChecked] = useState(false); - const [isFloodHistoryChecked, setIsFloodHistoryChecked] = useState(false); + const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false); const [swagePrcsFcltLayerMode, setSwagePrcsFcltLayerMode] = useState("all"); const [activeLayerSettingOrder, setActiveLayerSettingOrder] = useState([]); const [draftAgedPipeFilters, setDraftAgedPipeFilters] = @@ -2559,8 +2559,8 @@ const Map = () => { ); const [isSinkholePopupFilterActive, setIsSinkholePopupFilterActive] = useState(false); const [sinkholeRadiusMeters, setSinkholeRadiusMeters] = useState(SINKHOLE_DEFAULT_RADIUS); - const [selectedFloodHistoryDetail, setSelectedFloodHistoryDetail] = useState(null); - const [floodHistoryDetailPopupPosition, setFloodHistoryDetailPopupPosition] = useState<{ left: number; top: number } | null>(null); + const [selectedFldngHstryDetail, setSelectedFldngHstryDetail] = useState(null); + const [fldngHstryDetailPopupPosition, setFldngHstryDetailPopupPosition] = useState<{ left: number; top: number } | null>(null); const [selectedSinkholeDetail, setSelectedSinkholeDetail] = useState(null); const [selectedSinkholeNearbyPipes, setSelectedSinkholeNearbyPipes] = useState([]); const [lastSinkholeSyncSummary, setLastSinkholeSyncSummary] = useState(null); @@ -2593,8 +2593,8 @@ const Map = () => { const agedPipePopupWindowRef = useRef(null); const sinkholePopupWindowRef = useRef(null); // 침수발생이력 overlay layer는 지도 드래그 중에도 유지돼야 하므로 React state 대신 ref로 보관한다. - const floodHistoryAreaLayerRef = useRef | null>(null); - const floodHistoryDepthLayerRef = useRef | null>(null); + const fldngHstryAreaLayerRef = useRef | null>(null); + const fldngHstryDepthLayerRef = useRef | null>(null); const sinkholePopupCloseWatcherRef = useRef(null); const pipeAnalysisPopupWindowRef = useRef(null); const gisEditSessionPopupWindowRef = useRef(null); @@ -4289,10 +4289,10 @@ const Map = () => { [clearSinkholeFocusedPipeLayer, ensureSinkholeFocusedPipeLayer], ); - const ensureFloodHistoryAreaSource = useCallback(() => { + const ensureFldngHstryAreaSource = useCallback(() => { if (!mapReady || !gisMaps.current) return null; - if (!floodHistoryAreaLayerRef.current) { + if (!fldngHstryAreaLayerRef.current) { const source = new VectorSource(); const layer = new VectorLayer({ source, @@ -4303,16 +4303,16 @@ const Map = () => { layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); layer.set("clickPsblYn", "N"); gisMaps.current.addLayer(layer); - floodHistoryAreaLayerRef.current = layer; + fldngHstryAreaLayerRef.current = layer; } - return floodHistoryAreaLayerRef.current.getSource(); + return fldngHstryAreaLayerRef.current.getSource(); }, [mapReady]); - const ensureFloodHistoryDepthSource = useCallback(() => { + const ensureFldngHstryDepthSource = useCallback(() => { if (!mapReady || !gisMaps.current) return null; - if (!floodHistoryDepthLayerRef.current) { + if (!fldngHstryDepthLayerRef.current) { const source = new VectorSource(); const layer = new VectorLayer({ source, @@ -4323,21 +4323,21 @@ const Map = () => { layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); layer.set("clickPsblYn", "N"); gisMaps.current.addLayer(layer); - floodHistoryDepthLayerRef.current = layer; + fldngHstryDepthLayerRef.current = layer; } - return floodHistoryDepthLayerRef.current.getSource(); + return fldngHstryDepthLayerRef.current.getSource(); }, [mapReady]); - const clearFloodHistoryLayers = useCallback(() => { - floodHistoryAreaLayerRef.current?.getSource?.()?.clear?.(); - floodHistoryDepthLayerRef.current?.getSource?.()?.clear?.(); + const clearFldngHstryLayers = useCallback(() => { + fldngHstryAreaLayerRef.current?.getSource?.()?.clear?.(); + fldngHstryDepthLayerRef.current?.getSource?.()?.clear?.(); }, []); - const renderSelectedFloodHistory = useCallback( - (detail: FloodHistoryItem | null) => { - const areaSource = ensureFloodHistoryAreaSource(); - const depthSource = ensureFloodHistoryDepthSource(); + const renderSelectedFldngHstry = useCallback( + (detail: FldngHstryItem | null) => { + const areaSource = ensureFldngHstryAreaSource(); + const depthSource = ensureFldngHstryDepthSource(); if (!areaSource || !depthSource) return; areaSource.clear(); @@ -4348,21 +4348,21 @@ const Map = () => { const areaFeature = new Feature({ geometry: new Polygon([detail.areaRing]), }); - areaFeature.setStyle(createFloodHistoryAreaStyle(`${detail.floodYear}년 ${detail.districtName}`)); + areaFeature.setStyle(createFldngHstryAreaStyle(`${detail.floodYear}년 ${detail.districtName}`)); areaSource.addFeature(areaFeature); detail.depthZones.forEach((zone) => { const zoneFeature = new Feature({ geometry: new Polygon([zone.ring]), }); - zoneFeature.setStyle(createFloodHistoryDepthStyle(zone.depthLabel, zone.color)); + zoneFeature.setStyle(createFldngHstryDepthStyle(zone.depthLabel, zone.color)); depthSource.addFeature(zoneFeature); }); }, - [ensureFloodHistoryAreaSource, ensureFloodHistoryDepthSource], + [ensureFldngHstryAreaSource, ensureFldngHstryDepthSource], ); - const focusFloodHistoryOnMap = useCallback((detail: FloodHistoryItem) => { + const focusFldngHstryOnMap = useCallback((detail: FldngHstryItem) => { if (!gisMaps.current) return; const geometry = new Polygon([detail.areaRing]); @@ -4375,27 +4375,27 @@ const Map = () => { }); }, []); - const clearFloodHistorySelection = useCallback(() => { - setSelectedFloodHistoryDetail(null); - setFloodHistoryDetailPopupPosition(null); - clearFloodHistoryLayers(); - }, [clearFloodHistoryLayers]); + const clearFldngHstrySelection = useCallback(() => { + setSelectedFldngHstryDetail(null); + setFldngHstryDetailPopupPosition(null); + clearFldngHstryLayers(); + }, [clearFldngHstryLayers]); - const closeFloodHistoryDetail = useCallback(() => { - setFloodHistoryDetailPopupPosition(null); + const closeFldngHstryDetail = useCallback(() => { + setFldngHstryDetailPopupPosition(null); }, []); - const handleFloodHistoryRowSelect = useCallback( - (detail: FloodHistoryItem) => { - setSelectedFloodHistoryDetail(detail); - renderSelectedFloodHistory(detail); - focusFloodHistoryOnMap(detail); + const handleFldngHstryRowSelect = useCallback( + (detail: FldngHstryItem) => { + setSelectedFldngHstryDetail(detail); + renderSelectedFldngHstry(detail); + focusFldngHstryOnMap(detail); setIsSearchModalOpen(false); }, - [focusFloodHistoryOnMap, renderSelectedFloodHistory, setIsSearchModalOpen], + [focusFldngHstryOnMap, renderSelectedFldngHstry, setIsSearchModalOpen], ); - const openFloodHistoryPopupWindow = useCallback(() => { + const openFldngHstryPopupWindow = useCallback(() => { const popup = openWindowPopup(FLOOD_HISTORY_POPUP_KEY as WindowPopupKey); if (!popup) { alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요."); @@ -5105,16 +5105,16 @@ const Map = () => { }, [loadSinkholeDetail, sinkholeScope]); useEffect(() => { - const handleFloodHistoryPopupMessage = (event: MessageEvent) => { + const handleFldngHstryPopupMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin) return; - if (event.data?.type !== FLOOD_HISTORY_ROW_SELECT_MESSAGE || !event.data?.row) return; + if (event.data?.type !== FLDNG_HSTRY_ROW_SELECT_MESSAGE || !event.data?.row) return; - handleFloodHistoryRowSelect(event.data.row as FloodHistoryItem); + handleFldngHstryRowSelect(event.data.row as FldngHstryItem); }; - window.addEventListener("message", handleFloodHistoryPopupMessage); - return () => window.removeEventListener("message", handleFloodHistoryPopupMessage); - }, [handleFloodHistoryRowSelect]); + window.addEventListener("message", handleFldngHstryPopupMessage); + return () => window.removeEventListener("message", handleFldngHstryPopupMessage); + }, [handleFldngHstryRowSelect]); // WaterDepth 표시 여부를 결정하는 함수 const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { @@ -5171,10 +5171,10 @@ const Map = () => { WATER_DEPTH_LAYER_TYPES.includes(String(node?.data?.lyrTyNm ?? "")), ); setIsAgedPipeChecked(agedPipeChecked); - const floodHistoryChecked = checkedNodesData.some((node) => isFloodHistoryNode(node)); - setIsFloodHistoryChecked(floodHistoryChecked); - if (!floodHistoryChecked) { - clearFloodHistorySelection(); + const fldngHstryChecked = checkedNodesData.some((node) => isFldngHstryNode(node)); + setIsFldngHstryChecked(fldngHstryChecked); + if (!fldngHstryChecked) { + clearFldngHstrySelection(); } const sinkholeChecked = checkedNodesData.some((node) => isSinkholeLayerNode(node)); setIsSinkholeChecked(sinkholeChecked); @@ -5197,7 +5197,7 @@ const Map = () => { removeSinkholeLayer(); closeSinkholeDetail(); } - }, [clearFloodHistorySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]); + }, [clearFldngHstrySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]); const waterDepthLegendItems = useMemo( () => extractWaterDepthLegendItems(checkedNodesDataRef.current ?? []), @@ -5228,14 +5228,14 @@ const Map = () => { openSinkholePopupWindow(); return false; } - if (isFloodHistoryNode(node)) { - openFloodHistoryPopupWindow(); + if (isFldngHstryNode(node)) { + openFldngHstryPopupWindow(); return false; } return true; }, - [openAgedPipeList, openFloodHistoryPopupWindow, openSinkholePopupWindow], + [openAgedPipeList, openFldngHstryPopupWindow, openSinkholePopupWindow], ); // 현재 체크된 특수 레이어만 하단 설정영역에 모아 렌더링하기 위한 패널 목록이다. @@ -7110,25 +7110,25 @@ const Map = () => { }, [renderSelectedSinkhole, selectedSinkholeDetail, sinkholeRadiusMeters]); useEffect(() => { - renderSelectedFloodHistory(selectedFloodHistoryDetail); - }, [renderSelectedFloodHistory, selectedFloodHistoryDetail]); + renderSelectedFldngHstry(selectedFldngHstryDetail); + }, [renderSelectedFldngHstry, selectedFldngHstryDetail]); useEffect(() => { if (!mapReady || !gisMaps.current) return; const map = gisMaps.current; const clickKey = map.on("singleclick", (event: any) => { - if (!selectedFloodHistoryDetail || !isFloodHistoryChecked) return; + if (!selectedFldngHstryDetail || !isFldngHstryChecked) return; - let clickedFloodHistory = false; + let clickedFldngHstry = false; map.forEachFeatureAtPixel( event.pixel, (feature: any, layer: any) => { if ( - layer === floodHistoryAreaLayerRef.current || - layer === floodHistoryDepthLayerRef.current + layer === fldngHstryAreaLayerRef.current || + layer === fldngHstryDepthLayerRef.current ) { - clickedFloodHistory = true; + clickedFldngHstry = true; return feature; } return undefined; @@ -7136,14 +7136,14 @@ const Map = () => { { hitTolerance: 4 }, ); - if (!clickedFloodHistory) return; + if (!clickedFldngHstry) return; const size = map.getSize?.() ?? [1200, 800]; const popupWidth = 220; const popupHeight = 210; const nextLeft = Math.min(Math.max(event.pixel[0] + 12, 12), size[0] - popupWidth - 12); const nextTop = Math.min(Math.max(event.pixel[1] - 12, 12), size[1] - popupHeight - 12); - setFloodHistoryDetailPopupPosition({ + setFldngHstryDetailPopupPosition({ left: nextLeft, top: nextTop, }); @@ -7153,13 +7153,13 @@ const Map = () => { return () => { unByKey(clickKey); }; - }, [isFloodHistoryChecked, mapReady, selectedFloodHistoryDetail, setIsSearchModalOpen]); + }, [isFldngHstryChecked, mapReady, selectedFldngHstryDetail, setIsSearchModalOpen]); useEffect(() => { return () => { - clearFloodHistoryLayers(); + clearFldngHstryLayers(); }; - }, [clearFloodHistoryLayers]); + }, [clearFldngHstryLayers]); return ( @@ -7287,11 +7287,11 @@ const Map = () => {
) : null} - {selectedFloodHistoryDetail && floodHistoryDetailPopupPosition ? ( + {selectedFldngHstryDetail && fldngHstryDetailPopupPosition ? ( ) : null} { treeTogglePosition="center" zoomGuide={zoomGuide} layerIconVisible={(node: any) => - isAgedPipeLayerNode(node) || isSinkholeRootNode(node) || isFloodHistoryNode(node) + isAgedPipeLayerNode(node) || isSinkholeRootNode(node) || isFldngHstryNode(node) } defaultZoom={7.5} From 69c3183e35467693ec3bcb9d2bcc91a3d65f2224 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 24 Jul 2026 18:13:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?sn:=20=EC=B9=A8=EC=88=98=ED=9D=94=EC=A0=81?= =?UTF-8?q?=EB=8F=84=20=EC=A7=80=EB=8F=84=ED=91=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/common/table.ts | 61 +++++++++++++++- .../components/fldngHstry/fldngHstryTypes.ts | 7 +- .../popupComponent/featureClickEventPopup.tsx | 23 +++++- apps/sn/app/(map-layout)/map/page.tsx | 57 ++++++++++++--- .../(map-layout)/map/searchModal/basicTab.tsx | 3 + .../map/searchModal/fldngWtlvMap.tsx | 72 +++++++++++++++++++ .../map/searchModal/searchModal.tsx | 12 +++- .../map/searchModal/useBasicTab.ts | 35 ++++++++- apps/sn/middleware.ts | 1 + 9 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx 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 6818b45e4..6441b99c6 100644 --- a/apps/sn/app/(map-layout)/map/components/common/table.ts +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -478,6 +478,60 @@ export const SwtrmtPrcsZoneExcelCol = { 'swtrmtFcltNm': '하수처리시설명' }; +// 침수흔적도 +export const FldngWtlvMapCol = [ + { id: 'fldng_yr', title: '침수년도' }, + { id: 'fldng_dstr_nm', title: '침수재해명' }, + { id: 'ctpv_nm', title: '시도명' }, + { id: 'sgg_nm', title: '시군구명' }, + { id: 'bgng_ymd', title: '침수시작일자' }, + { id: 'fldng_bgng_hm', title: '침수시작시분' }, + { id: 'end_ymd', title: '침수종료일자' }, + { id: 'fldng_end_hm', title: '침수종료시분' }, + { id: 'fldng_hr', title: '침수시간' }, + { id: 'fldng_dstrct_nm', title: '침수지구명' }, + { id: 'fldng_cs_dtl_cd', title: '침수원인상세코드' }, + { id: 'avg_fldng_wtlv', title: '평균침수위' }, + { id: 'fldng_area', title: '침수면적' }, + { id: 'exmn_yr', title: '조사년도' }, + { id: 'dam_amt', title: '피해금액' }, +]; +export const FldngWtlvMapCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + { id: 'fldngYr', title: '침수년도' }, + { id: 'fldngDstrNm', title: '침수재해명' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + { id: 'bgngYmd', title: '침수시작일자' }, + { id: 'fldngBgngHm', title: '침수시작시분' }, + { id: 'endYmd', title: '침수종료일자' }, + { id: 'fldngEndHm', title: '침수종료시분' }, + { id: 'fldngHr', title: '침수시간' }, + { id: 'fldngDstrctNm', title: '침수지구명' }, + { id: 'fldngCsDtlCd', title: '침수원인상세코드' }, + { id: 'avgFldngWtlv', title: '평균침수위' }, + { id: 'fldngArea', title: '침수면적' }, + { id: 'exmnYr', title: '조사년도' }, + { id: 'damAmt', title: '피해금액' }, +]; +export const FldngWtlvMapExcelCol = { + 'fldngYr': '침수년도', + 'fldngDstrNm': '침수재해명', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'bgngYmd': '침수시작일자', + 'fldngBgngHm': '침수시작시분', + 'endYmd': '침수종료일자', + 'fldngEndHm': '침수종료시분', + 'fldngHr': '침수시간', + 'fldngDstrctNm': '침수지구명', + 'fldngCsDtlCd': '침수원인상세코드', + 'avgFldngWtlv': '평균침수위', + 'fldngArea': '침수면적', + 'exmnYr': '조사년도', + 'damAmt': '피해금액' +}; + // 하수맨홀 export const MsSwageMhlCol = [ // { id: 'fid', title: 'FID' }, @@ -819,7 +873,8 @@ export const dateFields = [ 'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd', 'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt', 'rainBeginDt', 'rainEndDt', - 'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr' + 'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr', + 'bgng_ymd', 'end_ymd', 'bgngYmd', 'endYmd' ]; // 🎯 연월 필드들 (yyyymm -> yyyy년 m월) @@ -848,5 +903,7 @@ export const numberFields = [ 'precdngDry', 'rainFall', 'cntncTime', 'rainIn', 'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc', 'bizAmt', 'count', - 'chmbr_xcnts', 'chmbr_ydnts' + 'chmbr_xcnts', 'chmbr_ydnts', + 'fldng_hr', 'avg_fldng_wtlv', 'fldng_area', 'dam_amt', + 'fldngHr', 'avgFldngWtlv', 'fldngArea', 'damAmt' ]; 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 94da67e29..4743f83a0 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -4,7 +4,8 @@ import type { UiHeadProps, UiTableSort } from "@common/business_components/ui"; 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_sn_flood_trace_mock"; +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_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA"; export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH"; @@ -442,7 +443,9 @@ const resolveFldngHstryLayerType = (node: any) => export const isFldngHstryNode = (node: any) => { const name = resolveFldngHstryNodeName(node); const layerType = resolveFldngHstryLayerType(node); - return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE; + return name === FLOOD_HISTORY_LAYER_NAME + || layerType === FLOOD_HISTORY_LAYER_TYPE + || layerType === FLOOD_HISTORY_PREVIEW_LAYER_TYPE; }; export const isFloodHistoryNode = isFldngHstryNode; 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 202f4e338..3a75126fa 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -32,7 +32,7 @@ import Fill from "ol/style/Fill.js"; import Stroke from "ol/style/Stroke.js"; import CircleStyle from "ol/style/Circle.js"; import { getLayerContentComponent } from "../common"; -import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table"; +import { FldngWtlvMapCol, FldngWtlvMapCol2, SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table"; import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; import { codeMapper } from "../common/codeMapper"; import LoadingOverlay from '@/components/ui/LoadingBar'; @@ -61,6 +61,12 @@ const linkedFacilityHighlightStyle = new Style({ zIndex: 9999, }); +const normalizeFldngWtlvMapDisplayId = (value: unknown) => { + const normalized = String(value ?? "").trim(); + const prefix = "tb_ms_fldng_wtlv_map."; + return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized; +}; + type MngInstOption = { mngInstCd: string; lwrInstNm: string; @@ -318,6 +324,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); break; } + case 'tb_ms_fldng_wtlv_map': { + result = '침수흔적도'; + setHeadlist(FldngWtlvMapCol); + setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.values_.fid ?? featureId)); + break; + } case 'tb_ms_stofcbr': { result = '우수토실'; setHeadlist(stofcbrCol); @@ -381,6 +393,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setLayerSpecificNm(featureData.swtrmtFcltCd); break; } + case 'tb_ms_fldng_wtlv_map': { + result = '침수흔적도'; + setHeadlist(FldngWtlvMapCol2); + setLayerSpecificNm(normalizeFldngWtlvMapDisplayId(featureData.fid)); + break; + } case 'tb_ms_stofcbr': { result = '우수토실'; setHeadlist(stofcbrCol2) @@ -478,6 +496,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, case '하수처리구역': setButtonChk(false) break; + case '침수흔적도': + setButtonChk(false) + break; case '우수토실': setButtonChk(true) setIsMainSsc(false) diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 83e99f778..d1fd26184 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -113,6 +113,7 @@ import { import { FLOOD_HISTORY_AREA_LAYER_ID, FLOOD_HISTORY_DEPTH_LAYER_ID, + FLOOD_HISTORY_PREVIEW_LAYER_TYPE, FLOOD_HISTORY_LAYER_TYPE, FLOOD_HISTORY_POPUP_KEY, FLDNG_HSTRY_ROW_SELECT_MESSAGE, @@ -141,6 +142,7 @@ const AnalysisPopupMenu = dynamic( const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE"; const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT"; +const FLDNG_HSTRY_WFS_LAYER_ID = "SN_FLDNG_HSTRY_WFS"; const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; @@ -911,12 +913,14 @@ const getRightPanelFeatureTitle = (data: any) => { return "펌프장"; case "tb_ms_stofcbr": return "우수토실"; - case "tb_il_swage_prcs_fclt": - return "처리시설"; - case "tb_ms_swage_prcs_zone": - return "처리구역"; - case "om_dt_untrt_swg_rain": - return "중점우수"; + case "tb_il_swage_prcs_fclt": + return "처리시설"; + case "tb_ms_swage_prcs_zone": + return "처리구역"; + case "tb_ms_fldng_wtlv_map": + return "침수흔적도"; + case "om_dt_untrt_swg_rain": + return "중점우수"; default: return String(data?.lyrNm ?? data?.values_?.lyrNm ?? "상세").trim() || "상세"; } @@ -4300,7 +4304,7 @@ const Map = () => { }); layer.set("id", FLOOD_HISTORY_AREA_LAYER_ID); layer.set("lyrNm", "침수흔적도"); - layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); + layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE); layer.set("clickPsblYn", "N"); gisMaps.current.addLayer(layer); fldngHstryAreaLayerRef.current = layer; @@ -4320,7 +4324,7 @@ const Map = () => { }); layer.set("id", FLOOD_HISTORY_DEPTH_LAYER_ID); layer.set("lyrNm", "침수흔적도"); - layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); + layer.set("lyrTyNm", FLOOD_HISTORY_PREVIEW_LAYER_TYPE); layer.set("clickPsblYn", "N"); gisMaps.current.addLayer(layer); fldngHstryDepthLayerRef.current = layer; @@ -5409,6 +5413,43 @@ const Map = () => { }; }, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]); + useEffect(() => { + if (!mapReady || !gisMaps.current) return; + + const core = GisApp.Module.core as any; + core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID); + + if (!isFldngHstryChecked) return; + + const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find( + (item: any) => item?.lyrTyNm === FLOOD_HISTORY_LAYER_TYPE && item?.actlLyrYn === "Y", + ) as any; + + if (!baseLayerCode) return; + + const layerCode = { + ...baseLayerCode, + id: FLDNG_HSTRY_WFS_LAYER_ID, + lyrSn: FLDNG_HSTRY_WFS_LAYER_ID, + lyrNm: "침수흔적도", + lyrTyNm: FLOOD_HISTORY_LAYER_TYPE, + actlLyrYn: "Y", + exprsYn: "Y", + clickPsblYn: "Y", + cmncNm: "bbox", + srvcNm: "wfs", + }; + + const layer = core?.addWfsLayer?.(layerCode); + layer?.set?.("id", FLDNG_HSTRY_WFS_LAYER_ID); + layer?.set?.("lyrNm", "침수흔적도"); + layer?.set?.("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE); + + return () => { + core?.deleteLayer?.(FLDNG_HSTRY_WFS_LAYER_ID); + }; + }, [isFldngHstryChecked, mapReady]); + const openAnalysisPopup = useCallback((popupKey: WindowPopupKey) => { const popup = openWindowPopup(popupKey); if (!popup) { diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx index 631ba12fb..f15cce8b0 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -14,6 +14,7 @@ import { Label } from '@common/components'; import SwagePpl from './swagePpl'; import SwagePrcsFclt from './swagePrcsFclt'; import SwagePrcsZone from './swagePrcsZone'; +import FldngWtlvMap from './fldngWtlvMap'; import SwageMhl from './swageMhl'; import SwagePmstn from './swagePmstn'; import Stofcbr from './stofcbr'; @@ -25,6 +26,7 @@ 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_swage_mhl", text: "하수맨홀" }, { val: "tb_ms_swage_pmstn", text: "하수펌프장" }, { val: "tb_ms_stofcbr", text: "우수토실" }, @@ -56,6 +58,7 @@ const BasicTab: React.FC = () => { {basicType === "tb_ms_swage_ppl" && ()} {/* 하수관로 */} {basicType === "tb_il_swage_prcs_fclt" && ()} {/* 하수처리시설 */} {basicType === "tb_ms_swage_prcs_zone" && ()} {/* 하수처리구역 */} + {basicType === "tb_ms_fldng_wtlv_map" && ()} {/* 침수흔적도 */} {basicType === "tb_ms_swage_mhl" && ()} {/* 하수맨홀 */} {basicType === "tb_ms_swage_pmstn" && ()} {/* 하수펌프장 */} {basicType === "tb_ms_stofcbr" && ()} {/* 우수토실 */} diff --git a/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx b/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx new file mode 100644 index 000000000..2a6c13428 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/fldngWtlvMap.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Button, Input, Label } from '@common/components'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import useSearchModalStore from '../../../store/useSearchModalStore'; +import useSearchModal from './useSearchModal'; + +// 침수흔적도용 CQL 필터 생성 +export const fldngWtlvMapCqlFilters = () => { + const { ctpvCd, sggCd, var1, var2 } = useSearchModalStore.getState(); + const cqlFilters: string[] = []; + if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`stdg_cd LIKE '${ctpvCd.trim()}%'`); + if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`stdg_cd LIKE '${sggCd.trim()}%'`); + if (var1) cqlFilters.push(`fldng_yr = '${var1.trim()}'`); + if (var2) cqlFilters.push(`fldng_dstr_nm ILIKE '%${var2.trim()}%'`); + return cqlFilters; +}; + +const FldngWtlvMap: React.FC = () => { + const { + var1, setVar1, var2, setVar2, + ctpvCd, setCtpvCd, + sggCd, setSggCd, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + return ( +
+
+ { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + + setVar1(e.target.value.replace(/[^0-9]/g, '').slice(0, 4))} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSearch(); + } + }} + /> + + setVar2(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSearch(); + } + }} + /> +
+ +
+
+
+ ); +}; + +export default React.memo(FldngWtlvMap); diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 1f3f2686d..f1c0b8249 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -18,7 +18,7 @@ import { Button } from '@common/components/ui'; import React, { useEffect, useMemo, useRef } from 'react'; import { Toaster } from 'react-hot-toast'; import useSearchModalStore from '@/store/useSearchModalStore'; -import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table'; +import { ExclnSewagePumpCol2, FldngWtlvMapCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table'; import BasicTab, { BASIC_TYPES } from './basicTab'; import OperationTab from './operationTab'; import ReportTab from './reportTab'; @@ -57,6 +57,13 @@ const BASIC_SORT_KEYS: Record> = { swtrmtZoneNm: 'swtrmt_zone_nm', ctpvNm: 'ctpv_nm', sggNm: 'sgg_nm', swtrmtFcltNm: 'swtrmt_fclt_nm', }, + tb_ms_fldng_wtlv_map: { + rowNumber: 'rowNumber', + fldngYr: 'fldng_yr', fldngDstrNm: 'fldng_dstr_nm', ctpvNm: 'stdg_cd', sggNm: 'stdg_cd', + bgngYmd: 'bgng_ymd', fldngBgngHm: 'fldng_bgng_hm', endYmd: 'end_ymd', fldngEndHm: 'fldng_end_hm', + fldngHr: 'fldng_hr', fldngDstrctNm: 'fldng_dstrct_nm', fldngCsDtlCd: 'fldng_cs_dtl_cd', + avgFldngWtlv: 'avg_fldng_wtlv', fldngArea: 'fldng_area', exmnYr: 'exmn_yr', damAmt: 'dam_amt', + }, tb_ms_swage_mhl: { rowNumber: 'rowNumber', fcltMngNo: 'fclt_mng_no', instlYmd: 'instl_ymd', swageMhlDiamVl: 'swage_mhl_diam_vl', @@ -138,9 +145,10 @@ const SearchModal: React.FC = ({ renderMode = 'launcher', auto const dynamicHeadlist: UiHeadProps[] = useMemo(() => { if (activeTab === 'basic') { - const headlist = basicType === 'tb_ms_swage_ppl' ? MsSwagePplCol2 + const headlist = basicType === 'tb_ms_swage_ppl' ? MsSwagePplCol2 : basicType === 'tb_il_swage_prcs_fclt' ? SwtrmtFcltCol2 : basicType === 'tb_ms_swage_prcs_zone' ? SwtrmtPrcsZoneCol2 + : basicType === 'tb_ms_fldng_wtlv_map' ? FldngWtlvMapCol2 : basicType === 'tb_ms_swage_mhl' ? MsSwageMhlCol2 : basicType === 'tb_ms_swage_pmstn' ? ExclnSewagePumpCol2 : basicType === 'tb_ms_stofcbr' ? stofcbrCol2 : []; diff --git a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts index 856b68661..a40c1cee9 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts +++ b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts @@ -15,7 +15,7 @@ import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; import { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, - MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol + FldngWtlvMapExcelCol, MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol } from '../components/common/table'; import { getData } from "@/components/lib/api"; import { BASIC_TYPES } from './basicTab'; @@ -28,6 +28,7 @@ import useSearchModalStore from '@/store/useSearchModalStore'; import { swagePplCqlFilters } from './swagePpl'; import { swagePrcsFcltCqlFilters } from './swagePrcsFclt'; import { swagePrcsZoneCqlFilters } from './swagePrcsZone'; +import { fldngWtlvMapCqlFilters } from './fldngWtlvMap'; import { swageMhlCqlFilters } from './swageMhl'; import { swagePmstnCqlFilters } from './swagePmstn'; import { stofcbrCqlFilters } from './stofcbr'; @@ -79,6 +80,7 @@ export default function useBasicTab() { if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters(); // 하수관로 else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설 else if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역 + else if (basicType === 'tb_ms_fldng_wtlv_map') cqlFilters = fldngWtlvMapCqlFilters(); // 침수흔적도 else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters(); // 하수맨홀 else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters(); // 하수펌프장 else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters(); // 우수토실 @@ -96,7 +98,7 @@ export default function useBasicTab() { let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? ''; if (!ctpvCd || !sggCd) { - const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); + const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? item?.stdgCd ?? formattedItem?.stdgCd ?? '')?.trim(); if (!ctpvCd && code) ctpvCd = code.slice(0, 2); if (!sggCd && code) sggCd = code.slice(0, 5); } @@ -214,6 +216,13 @@ export default function useBasicTab() { headerRange = "A1:D1"; break; } + case 'tb_ms_fldng_wtlv_map': { + // 침수흔적도 + cqlFilters = fldngWtlvMapCqlFilters(); + headerMapping = FldngWtlvMapExcelCol; + headerRange = "A1:O1"; + break; + } case 'tb_ms_swage_mhl': { // 하수맨홀 cqlFilters = swageMhlCqlFilters(); @@ -255,7 +264,7 @@ export default function useBasicTab() { let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? ''; if (!ctpvCd || !sggCd) { - const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); + const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? item?.stdgCd ?? formattedItem?.stdgCd ?? '')?.trim(); if (!ctpvCd && code) ctpvCd = code.slice(0, 2); if (!sggCd && code) sggCd = code.slice(0, 5); } @@ -311,6 +320,26 @@ export default function useBasicTab() { swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-' }; } + case 'tb_ms_fldng_wtlv_map': { + // 침수흔적도 + return { + fldngYr: formattedItem.fldngYr ?? item.fldngYr ?? '-', + fldngDstrNm: formattedItem.fldngDstrNm ?? item.fldngDstrNm ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', + bgngYmd: formattedItem.bgngYmd ?? item.bgngYmd ?? '-', + fldngBgngHm: formattedItem.fldngBgngHm ?? item.fldngBgngHm ?? '-', + endYmd: formattedItem.endYmd ?? item.endYmd ?? '-', + fldngEndHm: formattedItem.fldngEndHm ?? item.fldngEndHm ?? '-', + fldngHr: formattedItem.fldngHr ?? item.fldngHr ?? '-', + fldngDstrctNm: formattedItem.fldngDstrctNm ?? item.fldngDstrctNm ?? '-', + fldngCsDtlCd: formattedItem.fldngCsDtlCd ?? item.fldngCsDtlCd ?? '-', + avgFldngWtlv: formattedItem.avgFldngWtlv ?? item.avgFldngWtlv ?? '-', + fldngArea: formattedItem.fldngArea ?? item.fldngArea ?? '-', + exmnYr: formattedItem.exmnYr ?? item.exmnYr ?? '-', + damAmt: formattedItem.damAmt ?? item.damAmt ?? '-', + }; + } case 'tb_ms_swage_mhl': { // 하수맨홀 return { diff --git a/apps/sn/middleware.ts b/apps/sn/middleware.ts index f0a6733d6..2eddd4de5 100644 --- a/apps/sn/middleware.ts +++ b/apps/sn/middleware.ts @@ -71,6 +71,7 @@ export function middleware(req: NextRequest) { if ( typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 + || typename == "hasudo:tb_ms_fldng_wtlv_map" //침수흔적도 || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 ) {