From 0cbbc3b93461da820e1c161fbebc035f2fd32f86 Mon Sep 17 00:00:00 2001 From: mind-one001 Date: Thu, 20 Aug 2026 14:57:20 +0900 Subject: [PATCH] =?UTF-8?q?feat(sn):=20CS=20=EA=B8=B0=EC=A4=80=20UI=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=A7=80=EB=8F=84=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=ED=91=9C=EC=A4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/components/InputSection.tsx | 63 +- .../agedPipe/AgedPipeLayerSettingPanel.tsx | 41 +- .../components/agedPipe/AgedPipePopupPage.tsx | 371 +- .../map/components/agedPipe/agedPipeUtils.ts | 16 +- .../AgedPipeStatusByCtpvPopupPage.tsx | 207 +- .../agedPipeStatusByCtpvUtils.ts | 55 +- .../analysisMenu/AnalysisPopupMenu.tsx | 53 +- .../map/components/common/table.ts | 16 +- .../components/exmn/ExmnLayerSettingPanel.tsx | 21 +- .../exmn/ExmnSurveyResultSection.tsx | 30 +- .../fldngHstry/FldngHstryDetailPanel.tsx | 146 +- .../fldngHstry/FldngHstryPopupPage.tsx | 343 +- .../components/fldngHstry/fldngHstryTypes.ts | 114 +- .../floodDngrMap/FloodDngrMapPopupPage.tsx | 325 +- .../floodDngrMap/floodDngrMapTypes.ts | 73 +- .../gisEdit/GisEditPipeSessionPopup.tsx | 253 +- .../gisEdit/GisEditSessionPanel.tsx | 105 +- .../map/components/gisEdit/GisEditToolbar.tsx | 91 +- .../gisManagement/GisManagementPopupPage.tsx | 529 ++- .../gisManagement/GisUploadPreviewMap.tsx | 305 +- .../inftwtResult/InftwtResultPopupPage.tsx | 358 +- .../inftwtResult/useInftwtResultMap.ts | 10 +- .../PipeAnalysisDetailPopupPage.tsx | 443 +- .../pipeAnalysis/PipeAnalysisPopupPage.tsx | 877 ++-- .../pipeAnalysis/pipeAnalysisUtils.ts | 109 +- .../map/components/popupComponent/Ssc.tsx | 272 +- .../components/popupComponent/SwtrmtFclt.tsx | 2 + .../popupComponent/SwtrmtPrcsZoneAnalysis.tsx | 217 +- .../popupComponent/chart/infilChart.tsx | 345 +- .../popupComponent/chart/rdiiChart.tsx | 256 +- .../popupComponent/featureClickEventPopup.tsx | 605 ++- .../map/components/popupComponent/smh.tsx | 12 +- .../rdiiResult/RdiiResultPopupPage.tsx | 358 +- .../components/rdiiResult/useRdiiResultMap.ts | 8 +- .../repairHistory/RepairHistoryPopupPage.tsx | 485 +- .../sbmrgDngrMap/SbmrgDngrMapPopupPage.tsx | 259 +- .../sbmrgDngrMap/sbmrgDngrMapTypes.ts | 61 +- .../sinkhole/SinkholeDetailPanel.tsx | 48 +- .../sinkhole/SinkholeLayerSettingPanel.tsx | 86 +- .../components/sinkhole/SinkholePopupPage.tsx | 410 +- .../map/components/sinkhole/sinkholeTypes.ts | 67 +- .../waterRisk/WaterRiskPopupPage.tsx | 663 +-- apps/sn/app/(map-layout)/map/page.tsx | 4049 +++++++++++------ .../(map-layout)/map/searchModal/basicTab.tsx | 80 +- .../map/searchModal/fldngWtlvMap.tsx | 96 +- .../map/searchModal/operationTab.tsx | 100 +- .../map/searchModal/reportTab.tsx | 503 +- .../map/searchModal/searchModal.tsx | 414 +- .../(map-layout)/map/searchModal/stofcbr.tsx | 72 +- .../(map-layout)/map/searchModal/swageMhl.tsx | 186 +- .../map/searchModal/swagePmstn.tsx | 158 +- .../(map-layout)/map/searchModal/swagePpl.tsx | 194 +- .../map/searchModal/swagePrcsFclt.tsx | 173 +- .../map/searchModal/swagePrcsZone.tsx | 113 +- .../map/searchModal/useBasicTab.ts | 488 +- .../map/searchModal/useOperationTab.ts | 398 +- .../map/searchModal/useReportTab.ts | 2 + .../map/searchModal/useSearchModal.ts | 132 +- .../map/waterDepth/waterDepth.tsx | 104 +- apps/sn/app/(popupLayout)/layout.tsx | 4 +- .../(popupLayout)/popup/[popupKey]/page.tsx | 13 +- .../app/(with-base-layout)/ii/BaseLayout.tsx | 277 +- .../ii/analysisResultView.ts | 15 +- .../(with-base-layout)/ii/fileupload/page.tsx | 191 +- .../(with-base-layout)/ii/irsetting/page.tsx | 553 ++- .../app/(with-base-layout)/ii/rdii/page.tsx | 502 +- .../(with-base-layout)/ii/rdiiResult/page.tsx | 313 +- .../app/(with-base-layout)/ii/result/page.tsx | 314 +- .../ii/useAnalysisCompletionWatcher.tsx | 84 +- apps/sn/app/(with-base-layout)/layout.tsx | 22 +- .../components/DredgingDetailPopupPage.tsx | 506 +- .../components/DredgingLocationMap.tsx | 11 +- .../components/DredgingRegisterModal.tsx | 706 +-- .../oper/dredging/lib/client.ts | 1 + .../oper/dredging/lib/types.ts | 2 + .../(with-base-layout)/oper/dredging/page.tsx | 499 +- .../oper/floodPre/headMakeFloodPre.tsx | 21 +- .../oper/floodPre/inputTableBody.tsx | 37 +- .../(with-base-layout)/oper/floodPre/page.tsx | 466 +- .../oper/floodPre/tableModal.tsx | 497 +- .../(with-base-layout)/oper/oldPp/page.tsx | 544 ++- .../oper/oldPp/tableModal.tsx | 810 ++-- .../oper/operStore/page.tsx | 30 +- .../selectionValidation.tsx | 57 +- .../sn/app/components/ui/LoadingBar/index.tsx | 11 +- .../app/components/ui/YearRangeSelectBox.tsx | 40 +- .../ui/ctpvSggPrcsFcltSelectBox.tsx | 118 +- apps/sn/app/components/ui/fileUploadForm.tsx | 119 +- .../app/components/ui/operFileUploadForm.tsx | 71 +- apps/sn/app/components/ui/toast.tsx | 21 +- apps/sn/app/components/ui/verticalTable.tsx | 178 +- apps/sn/app/store/useSearchModalStore.ts | 30 +- .../001-fe-SFR-010-004~007-조사사업-통합구현기록.md | 80 + .../001-fe-SFR-010-012-준설관리-구현기록.md | 56 + .../001-fe-SN-CS-UI통일-2차-구현기록.md | 117 + .../002-fe-SN-전체화면-인벤토리-검증기록.md | 127 + .../003-fe-2026-08-12-SN-CS-UI통일-작업내용.md | 113 + .../004-fe-2026-08-13-SN-CS-UI통일-작업내용.md | 149 + .../001-fe-SN-CS사용자메시지통일-구현기록.md | 64 + .../001-fe-SN-노후하수관로정비사업-통합구현기록.md | 91 + apps/sn/scripts/audit-screen-inventory.mjs | 102 + apps/sn/src/app/styles/components.css | 160 +- .../cfmtnBgt/ui/CfmtnBgtPopupContent.tsx | 857 ++-- .../ui/CtyFldngPrvtnBizFormPage.tsx | 400 +- .../ui/CtyFldngPrvtnBizListPage.tsx | 598 ++- .../features/exmn/api/exmn-business-api.ts | 136 +- .../src/features/exmn/model/trtdst-types.ts | 2 +- .../exmn/ui/ExmnBusinessRegisterPopupPage.tsx | 508 ++- .../exmn/ui/ExmnPplUnitDetailTable.tsx | 257 +- .../features/exmn/ui/ExmnRegistrationPage.tsx | 786 ++-- .../exmn/ui/ExmnResultRegisterPopupPage.tsx | 558 ++- .../exmn/ui/ExmnStandardDataTable.tsx | 143 + .../exmn/ui/ExmnStatusDetailPanel.tsx | 389 +- .../exmn/ui/ExmnSurveyDetailPopupPage.tsx | 1075 ++--- .../exmn/ui/PplFacilityDetailModal.tsx | 182 +- .../src/features/exmn/ui/PplSummaryModal.tsx | 500 +- .../features/exmn/ui/TrtdstManagementPage.tsx | 623 ++- .../features/obsltPpl/ui/ObsltPplFormPage.tsx | 416 +- .../features/obsltPpl/ui/ObsltPplListPage.tsx | 592 ++- .../windowPopup/model/popupDefinitions.ts | 34 +- apps/sn/src/features/windowPopup/registry.tsx | 158 +- .../ui/DredgingEditPopupContent.tsx | 2 +- .../ui/DredgingRegisterPopupContent.tsx | 2 +- .../ui/ExmnBusinessRegisterPopupContent.tsx | 8 +- .../ui/ExmnPipeReportPopupContent.tsx | 41 +- .../ui/ExmnResultRegisterPopupContent.tsx | 12 +- .../ui/ExmnSurveyDetailPopupContent.tsx | 9 +- .../ui/FeaturePlaceholderPopupContent.tsx | 23 +- .../ui/FloodPreTablePopupContent.tsx | 27 +- .../ui/FloodPreUploadPopupContent.tsx | 23 +- .../ui/GisEditSessionPopupContent.tsx | 61 +- .../ui/InftwtResultPopupContent.tsx | 4 +- .../windowPopup/ui/MapPopupDataTable.tsx | 182 + .../windowPopup/ui/OldPpTablePopupContent.tsx | 27 +- .../ui/OldPpUploadPopupContent.tsx | 23 +- .../windowPopup/ui/PopupMissingParams.tsx | 23 +- .../windowPopup/ui/PopupSearchPanel.tsx | 32 + .../windowPopup/ui/RdiiResultPopupContent.tsx | 4 +- .../ui/ReportFilesPopupContent.tsx | 60 +- .../windowPopup/ui/ReportTabPopupContent.tsx | 14 +- .../windowPopup/ui/WindowPopupRenderer.tsx | 22 + ...opupContent.tsx => dredgingPopupRecord.ts} | 17 +- apps/sn/src/shared/lib/bbs-notice-menu.ts | 14 +- apps/sn/src/shared/lib/statistics-menu-url.ts | 112 +- package.json | 1 + .../common/business_components/package.json | 3 +- .../src/ui/date_month_picker/mothPicker.tsx | 47 +- .../src/ui/excel/excelStyle.tsx | 160 +- .../src/ui/form_ui/index.tsx | 2 + .../business_components/src/ui/index.ts | 5 +- .../src/ui/loadingBar/index.tsx | 1 + .../src/ui/map/searchBar/SearchInputText.tsx | 14 +- .../src/ui/map/searchBar/procSearchBar.tsx | 144 +- .../src/ui/map/searchBar/searchBar.tsx | 58 +- .../src/ui/sdmInsertForm/form.tsx | 368 +- .../src/ui/sdmInsertForm/index.tsx | 243 +- .../src/ui/sdmInsertForm/sdmNameBox.tsx | 26 +- .../ui/search_address/SearchAddressModal.tsx | 83 +- .../src/ui/search_input_address/index.tsx | 10 +- .../src/ui/select/index.tsx | 2 + .../src/ui/select/lib/selectBox.tsx | 72 +- .../src/ui/standard_data_table/index.tsx | 316 ++ .../standard-data-table.css | 23 + .../src/ui/standard_form/index.tsx | 110 + .../src/ui/standard_popup/index.tsx | 85 + .../src/ui/table/HeadMakeColSpan.tsx | 12 +- .../src/ui/table/index.tsx | 119 +- 167 files changed, 21191 insertions(+), 14161 deletions(-) create mode 100644 apps/sn/docs/작업내용/SN-CS-UI통일-2차/001-fe-SN-CS-UI통일-2차-구현기록.md create mode 100644 apps/sn/docs/작업내용/SN-CS-UI통일-2차/002-fe-SN-전체화면-인벤토리-검증기록.md create mode 100644 apps/sn/docs/작업내용/SN-CS-UI통일-2차/003-fe-2026-08-12-SN-CS-UI통일-작업내용.md create mode 100644 apps/sn/docs/작업내용/SN-CS-UI통일-2차/004-fe-2026-08-13-SN-CS-UI통일-작업내용.md create mode 100644 apps/sn/docs/작업내용/SN-CS사용자메시지통일/001-fe-SN-CS사용자메시지통일-구현기록.md create mode 100644 apps/sn/scripts/audit-screen-inventory.mjs create mode 100644 apps/sn/src/features/exmn/ui/ExmnStandardDataTable.tsx create mode 100644 apps/sn/src/features/windowPopup/ui/MapPopupDataTable.tsx create mode 100644 apps/sn/src/features/windowPopup/ui/PopupSearchPanel.tsx create mode 100644 apps/sn/src/features/windowPopup/ui/WindowPopupRenderer.tsx rename apps/sn/src/features/windowPopup/ui/{DredgingDetailPopupContent.tsx => dredgingPopupRecord.ts} (83%) create mode 100644 packages/common/business_components/src/ui/standard_data_table/index.tsx create mode 100644 packages/common/business_components/src/ui/standard_data_table/standard-data-table.css create mode 100644 packages/common/business_components/src/ui/standard_form/index.tsx create mode 100644 packages/common/business_components/src/ui/standard_popup/index.tsx diff --git a/apps/sn/app/(map-layout)/map/components/InputSection.tsx b/apps/sn/app/(map-layout)/map/components/InputSection.tsx index 1d45103a1..68c4a3a83 100644 --- a/apps/sn/app/(map-layout)/map/components/InputSection.tsx +++ b/apps/sn/app/(map-layout)/map/components/InputSection.tsx @@ -1,49 +1,78 @@ -import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui"; +import { + Table, + TableHeader, + TableRow, + TableHead, + TableBody, + TableCell, +} from "@common/components/ui"; import React from "react"; interface InputSectionProps { title: string; - data: { year: string; type: string, bizInfoNm: string, bizType: string }[]; + data: { year: string; type: string; bizInfoNm: string; bizType: string }[]; sectionKey: string; - onInputChange: (section: string, index: number, field: string, value: string) => void; + onInputChange: ( + section: string, + index: number, + field: string, + value: string, + ) => void; } -const InputSection: React.FC = ({ title, data, sectionKey, onInputChange }) => { +const InputSection: React.FC = ({ + title, + data, + sectionKey, + onInputChange, +}) => { return (
{/* 섹션 제목 */}
-

{title}

+

+ {title} +

{/* 테이블 */} -
+
- 구분 - 진단연도 - 사업명 - 유형 + + 구분 + + + 진단연도 + + + 사업명 + + + 유형 + {data.map((row, index) => ( - + {row.type} - + onInputChange(sectionKey, index, "year", e.target.value)} - className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal" + onChange={(e) => + onInputChange(sectionKey, index, "year", e.target.value) + } + className="border-radius-0 w-full px-3 py-2 text-center text-base text-xl leading-normal" /> - + {row.bizInfoNm} - + {row.bizType} @@ -55,4 +84,4 @@ const InputSection: React.FC = ({ title, data, sectionKey, on ); }; -export default InputSection; \ No newline at end of file +export default InputSection; diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx index c4c3db13c..9a6174db1 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipeLayerSettingPanel.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useAlertStore } from "@/shared/lib"; import { validateElapsedYears, type AgedPipeFilters } from "./agedPipeUtils"; type MaybePromise = T | Promise; @@ -18,15 +19,19 @@ export default function AgedPipeLayerSettingPanel({ onApply, compact = false, }: Props) { + const alert = useAlertStore((state) => state.setMessage); const [isCollapsed, setIsCollapsed] = useState(false); - const [elapsedYearsText, setElapsedYearsText] = useState(String(filters.elapsedYears ?? "")); + const [elapsedYearsText, setElapsedYearsText] = useState( + String(filters.elapsedYears ?? ""), + ); useEffect(() => { setElapsedYearsText(String(filters.elapsedYears ?? "")); }, [filters.elapsedYears]); const commitElapsedYears = async () => { - const elapsedYears = elapsedYearsText === "" ? NaN : Number(elapsedYearsText); + const elapsedYears = + elapsedYearsText === "" ? NaN : Number(elapsedYearsText); if (!validateElapsedYears(elapsedYears)) { alert("경과년도는 1년~100년 범위 내에서 입력해주세요."); return; @@ -41,16 +46,22 @@ export default function AgedPipeLayerSettingPanel({ alert(`노후관로 경과년도 ${elapsedYears}년으로 반영되었습니다.`); }; - const inputClass = "h-[22px] border border-[#ccd6df] px-[2px] text-center text-[1rem] leading-none text-[#222]"; + const inputClass = + "h-[22px] border border-input bg-background px-[2px] text-center text-[1rem] leading-none text-foreground"; const numberInputClass = inputClass + " w-[40px]"; - const applyButtonClass = "grid h-[20px] w-[20px] shrink-0 place-items-center rounded-full bg-[#31a83b] text-[0.95rem] font-bold leading-none text-white"; - const indicator = ; + const applyButtonClass = + "grid h-[20px] w-[20px] shrink-0 place-items-center rounded-full bg-primary text-[0.95rem] font-bold leading-none text-primary-foreground"; + const indicator = ( + + ); const settingRow = ( -
+
{indicator} - +
+
{settingRow}
); @@ -93,23 +104,21 @@ export default function AgedPipeLayerSettingPanel({ return (
-
+
- 노후관로 설정 + + 노후관로 설정 +
- {!isCollapsed && ( -
- {settingRow} -
- )} + {!isCollapsed &&
{settingRow}
}
); } diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx index 28a1e443d..e5a180fbc 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx @@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { Button, Input } from "@common/components"; -import { UiTable } from "@common/business_components/ui"; -import type { UiTableSort } from "@common/business_components/ui"; +import { StandardPopupLayout } from "@common/business_components"; import { getData } from "@/components/lib/api"; +import { useAlertStore } from "@/shared/lib"; import useStore from "@/store/useStore"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { @@ -21,38 +21,23 @@ import { type AgedPipeRow, } from "./agedPipeUtils"; import { downloadAgedPipeCsv } from "./agedPipeExport"; +import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; +import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; -const AGED_PIPE_API_SORT_KEYS: Record = { - rowNumber: "rowNumber", - fcltMngNo: "fclt_mng_no", - instlYmd: "instl_ymd", - pdmtVl: "pdmt_vl", - prlgVl: "prlg_vl", - usgCd: "usg_cd", - mqltCd: "mqlt_cd", -}; - -const resolveAgedPipeApiSort = (sort?: UiTableSort) => { - if (!sort) return undefined; - if (sort.key === "elapsedYears") { - const installDateDirection = sort.direction === "asc" ? "desc" : "asc"; - return `instl_ymd,${installDateDirection}`; - } - - const apiKey = AGED_PIPE_API_SORT_KEYS[sort.key]; - return apiKey ? `${apiKey},${sort.direction}` : undefined; -}; - export default function AgedPipePopupPage() { + const alert = useAlertStore((state) => state.setMessage); const searchParams = useSearchParams(); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); const [filters, setFilters] = useState(() => normalizeAgedPipeFilters({ - elapsedYears: Number(searchParams.get("elapsedYears") ?? AGED_PIPE_DEFAULT_FILTERS.elapsedYears), + elapsedYears: Number( + searchParams.get("elapsedYears") ?? + AGED_PIPE_DEFAULT_FILTERS.elapsedYears, + ), ctpvCd: searchParams.get("ctpvCd") ?? AGED_PIPE_DEFAULT_FILTERS.ctpvCd, sggCd: searchParams.get("sggCd") ?? AGED_PIPE_DEFAULT_FILTERS.sggCd, }), @@ -60,16 +45,16 @@ export default function AgedPipePopupPage() { 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 [downloadProgressText, setDownloadProgressText] = useState(""); - const [sort, setSort] = useState(); const sggOptions = useMemo( () => filters.ctpvCd === "0" ? sgg - : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))), + : sgg.filter((item) => + String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), + ), [filters.ctpvCd, sgg], ); @@ -79,20 +64,13 @@ export default function AgedPipePopupPage() { }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]); const fetchRows = useCallback( - async ( - page: number, - nextFilters: AgedPipeFilters, - size: number, - activeSort?: UiTableSort, - ) => { + async (page: number, nextFilters: AgedPipeFilters, size: number) => { const normalized = normalizeAgedPipeFilters(nextFilters); const queryParams = new URLSearchParams({ lyrTypeNm: AGED_PIPE_LAYER_TYPE, page: String(page - 1), size: String(size), }); - const apiSort = resolveAgedPipeApiSort(activeSort); - if (apiSort) queryParams.set("sort", apiSort); const cqlFilters = buildAgedPipeCqlFilters(normalized); if (cqlFilters.length > 0) { queryParams.append("cqlFiter", cqlFilters.join(" and ")); @@ -102,15 +80,10 @@ export default function AgedPipePopupPage() { const responseRows = Array.isArray(response?.data) ? response.data : []; const total = Number(response?.count ?? 0); - const descendingSequence = - activeSort?.key === "rowNumber" && activeSort.direction === "desc"; - const rows = responseRows.map((item: Record, index: number) => { - const row = toDisplayPipeRow(item, index, page, ctpv, sgg); - if (descendingSequence) { - row.rowNumber = total - ((page - 1) * size + index); - } - return row; - }); + const rows = responseRows.map( + (item: Record, index: number) => + toDisplayPipeRow(item, index, page, ctpv, sgg), + ); return { rows, total }; }, @@ -118,10 +91,14 @@ export default function AgedPipePopupPage() { ); const loadRows = useCallback( - async (page = 1, nextFilters = filters, activeSort = sort) => { + async (page = 1, nextFilters = filters) => { setIsLoading(true); try { - const response = await fetchRows(page, nextFilters, AGED_PIPE_PAGE_SIZE, activeSort); + const response = await fetchRows( + page, + nextFilters, + AGED_PIPE_PAGE_SIZE, + ); setRows(response.rows); setTotal(response.total); setCurrentPage(page); @@ -133,7 +110,7 @@ export default function AgedPipePopupPage() { setIsLoading(false); } }, - [fetchRows, filters, sort], + [fetchRows, filters], ); useEffect(() => { @@ -148,9 +125,10 @@ export default function AgedPipePopupPage() { if (event.data?.type !== POPUP_FILTER_SYNC_MESSAGE) return; if (!event.data?.filters) return; - const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters); + const normalized = normalizeAgedPipeFilters( + event.data.filters as AgedPipeFilters, + ); setFilters(normalized); - setSelectedIndex(-1); loadRows(1, normalized); }; @@ -169,12 +147,7 @@ export default function AgedPipePopupPage() { await loadRows(page, normalized); }; - const handleRowClick = (index?: number) => { - if (typeof index !== "number") return; - const row = rows[index]; - if (!row) return; - - setSelectedIndex(index); + const handleRowClick = (row: AgedPipeRow) => { window.opener?.postMessage( { type: POPUP_ROW_SELECT_MESSAGE, @@ -184,12 +157,6 @@ export default function AgedPipePopupPage() { ); }; - const handleSortChange = (nextSort?: UiTableSort) => { - setSort(nextSort); - setSelectedIndex(-1); - void loadRows(1, filters, nextSort); - }; - const handleDownload = async () => { if (total === 0) { alert("다운로드할 데이터가 없습니다."); @@ -237,183 +204,133 @@ export default function AgedPipePopupPage() { window.close(); }; - const getCellClassName = (_row: AgedPipeRow, headId: string) => { - if (headId === "mqltCd") { - return "break-all whitespace-normal"; - } - - return "whitespace-normal"; - }; - return ( -
+ + 최소화 + + } + > -
-

노후관로 현황

-
- -
-
{downloadProgressText ? ( -
+
{downloadProgressText}
) : null} -
-
- - - - - - -
- - -
-
- -
- - headName={<>노후관로 조회 결과 총 {total.toLocaleString()}건} - tableData={rows} - headlist={AGED_PIPE_LIST_HEADLIST} - pageSize={10} - total={total} - cellClick={(index) => handleRowClick(index)} - onPageChange={(page) => void handleSearch(page)} - cellClassName={getCellClassName} - curPage={currentPage} - index={selectedIndex} - widthFixed={true} - isHideCount={true} - className="!m-0 overflow-x-hidden" - sort={sort} - onSortChange={handleSortChange} + + + + + + + +
+ +
-
- -
+ /> + ); } diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts index 0c5763239..736bb9478 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts @@ -54,14 +54,14 @@ const normalizeRegionFilterCode = (value: unknown, prefixLength: number) => { }; export const AGED_PIPE_LIST_HEADLIST = [ - { id: "rowNumber", title: "NO", widthPercent: 6, sortable: true }, - { id: "fcltMngNo", title: "시설관리번호", widthPercent: 18, sortable: true }, - { id: "instlYmd", title: "설치일자", widthPercent: 12, sortable: true }, - { id: "pdmtVl", title: "관경(mm)", widthPercent: 10, sortable: true }, - { id: "prlgVl", title: "연장(m)", widthPercent: 10, sortable: true }, - { id: "usgCd", title: "용도", widthPercent: 12, sortable: true }, - { id: "mqltCd", title: "재질", widthPercent: 18, sortable: true }, - { id: "elapsedYears", title: "경과년도", widthPercent: 8, align: "right", sortable: true }, + { id: "rowNumber", title: "순번", widthPercent: 6 }, + { id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 }, + { id: "instlYmd", title: "설치일자", widthPercent: 12 }, + { id: "pdmtVl", title: "관경(mm)", widthPercent: 10 }, + { id: "prlgVl", title: "연장(m)", widthPercent: 10 }, + { id: "usgCd", title: "용도", widthPercent: 12 }, + { id: "mqltCd", title: "재질", widthPercent: 18 }, + { id: "elapsedYears", title: "경과년도", widthPercent: 8, align: "right" }, ]; export const AGED_PIPE_EXCEL_FIELDS = [ diff --git a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx index 7b4dcc924..e9ba16d7b 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx @@ -2,12 +2,13 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Input } from "@common/components"; -import { UiTable } from "@common/business_components/ui"; +import { StandardPopupLayout } from "@common/business_components"; import { getRawJson } from "@/components/lib/api"; import LoadingOverlay from "@/components/ui/LoadingBar"; +import { useAlertStore } from "@/shared/lib"; +import { windowPopupDefinitions } from "@/features/windowPopup/model/popupDefinitions"; import { AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS, - AGED_PIPE_STATUS_BY_CTPV_PAGE_SIZE, buildAgedPipeStatusByCtpvHeadList, buildAgedPipeStatusYearOptions, normalizeAgedPipeStatusByCtpvResponse, @@ -19,8 +20,14 @@ import { type AgedPipeStatusByCtpvRow, } from "./agedPipeStatusByCtpvUtils"; import { downloadAgedPipeStatusByCtpvCsv } from "./agedPipeStatusByCtpvExport"; +import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; +import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; + +// CS 목록과 동일하게 한 페이지에는 최대 10개 시도만 표시한다. +const AGED_PIPE_STATUS_PAGE_SIZE = 10; export default function AgedPipeStatusByCtpvPopupPage() { + const alert = useAlertStore((state) => state.setMessage); // 조회조건 입력 상태다. 기본값 30년, 최근 연도 기준 단년 조회로 시작한다. const [filters, setFilters] = useState(() => normalizeAgedPipeStatusByCtpvFilters({ @@ -35,33 +42,54 @@ export default function AgedPipeStatusByCtpvPopupPage() { const [isLoading, setIsLoading] = useState(false); // 사용자에게 현재 다운로드 상태를 보여주는 진행 문구다. const [downloadProgressText, setDownloadProgressText] = useState(""); + const [currentPage, setCurrentPage] = useState(1); const yearOptions = useMemo(() => buildAgedPipeStatusYearOptions(), []); - const headList = useMemo(() => buildAgedPipeStatusByCtpvHeadList(years), [years]); + const headList = useMemo( + () => [ + { id: "rowNumber", title: "순번", widthPercent: 7 }, + ...buildAgedPipeStatusByCtpvHeadList(years), + ], + [years], + ); + const pageRows = useMemo( + () => + rows.slice( + (currentPage - 1) * AGED_PIPE_STATUS_PAGE_SIZE, + currentPage * AGED_PIPE_STATUS_PAGE_SIZE, + ), + [currentPage, rows], + ); - const loadRows = useCallback(async (nextFilters: AgedPipeStatusByCtpvFilters) => { - setIsLoading(true); - try { - const normalized = normalizeAgedPipeStatusByCtpvFilters(nextFilters); - const queryParams = new URLSearchParams({ - startYear: String(normalized.startYear), - endYear: String(normalized.endYear), - elapsedYears: String(normalized.elapsedYears), - }); - const rawResponse = await getRawJson( - `v1/swagePpl/aged-status-by-ctpv?${queryParams.toString()}`, - ); - const response = normalizeAgedPipeStatusByCtpvResponse(rawResponse) as AgedPipeStatusByCtpvResponse; - setYears(Array.isArray(response?.years) ? response.years : []); - setRows(toAgedPipeStatusByCtpvRows(response)); - } catch (error) { - console.error("시도별 노후관로현황 조회 중 오류:", error); - setYears([]); - setRows([]); - } finally { - setIsLoading(false); - } - }, []); + const loadRows = useCallback( + async (nextFilters: AgedPipeStatusByCtpvFilters) => { + setIsLoading(true); + try { + const normalized = normalizeAgedPipeStatusByCtpvFilters(nextFilters); + const queryParams = new URLSearchParams({ + startYear: String(normalized.startYear), + endYear: String(normalized.endYear), + elapsedYears: String(normalized.elapsedYears), + }); + const rawResponse = await getRawJson( + `v1/swagePpl/aged-status-by-ctpv?${queryParams.toString()}`, + ); + const response = normalizeAgedPipeStatusByCtpvResponse( + rawResponse, + ) as AgedPipeStatusByCtpvResponse; + setYears(Array.isArray(response?.years) ? response.years : []); + setRows(toAgedPipeStatusByCtpvRows(response)); + setCurrentPage(1); + } catch (error) { + console.error("시도별 노후관로현황 조회 중 오류:", error); + setYears([]); + setRows([]); + } finally { + setIsLoading(false); + } + }, + [], + ); useEffect(() => { void loadRows(filters); @@ -70,7 +98,9 @@ export default function AgedPipeStatusByCtpvPopupPage() { const handleSearch = async () => { const normalized = normalizeAgedPipeStatusByCtpvFilters(filters); if (!validateAgedPipeStatusByCtpvFilters(normalized)) { - alert("기준년도는 최대 3년 범위 내에서 선택하고, 경과년도는 1~100 사이로 입력해주세요."); + alert( + "기준년도는 최대 3년 범위 내에서 선택하고, 경과년도는 1~100 사이로 입력해주세요.", + ); return; } setFilters(normalized); @@ -97,25 +127,25 @@ export default function AgedPipeStatusByCtpvPopupPage() { }; return ( -
+ -
-

시도별 노후관로현황

-
-
{downloadProgressText ? ( -
+
{downloadProgressText}
) : null} -
-
- - - -
- - -
+
+ +
+ -
- - // 조회 결과 총 {rows.length.toLocaleString()}건 - headName={<>시도별 노후관로현황 } - tableData={rows} - headlist={headList} - pageSize={AGED_PIPE_STATUS_BY_CTPV_PAGE_SIZE} - total={rows.length} - cellClick={() => undefined} - onPageChange={() => undefined} - curPage={1} - index={-1} - widthFixed={true} - isHideCount={true} - cellClassName={() => "text-[1.2rem]"} - /> -
-
-
+ > + } + headlist={headList} + title="시도별 노후관로 목록" + total={rows.length} + page={currentPage} + pageSize={AGED_PIPE_STATUS_PAGE_SIZE} + onPageChange={setCurrentPage} + scrollMode={years.length > 2 ? "horizontal" : "none"} + minTableWidth={years.length > 2 ? "120rem" : "100%"} + actions={ + + } + /> + ); } diff --git a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts index d41f12d31..6a01cc9de 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts @@ -44,14 +44,21 @@ export const normalizeAgedPipeStatusByCtpvResponse = ( ): AgedPipeStatusByCtpvResponse => ({ startYear: Number(response?.startYear ?? 0), endYear: Number(response?.endYear ?? 0), - elapsedYears: Number(response?.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS), - years: Array.isArray(response?.years) ? response.years.map((year) => Number(year)) : [], + elapsedYears: Number( + response?.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS, + ), + years: Array.isArray(response?.years) + ? response.years.map((year) => Number(year)) + : [], rows: Array.isArray(response?.rows) ? response.rows : [], }); export const buildAgedPipeStatusYearOptions = () => { const currentYear = new Date().getFullYear(); - return Array.from({ length: currentYear - 1900 + 1 }, (_, index) => currentYear - index); + return Array.from( + { length: currentYear - 1900 + 1 }, + (_, index) => currentYear - index, + ); }; export const normalizeAgedPipeStatusByCtpvFilters = ( @@ -61,12 +68,18 @@ export const normalizeAgedPipeStatusByCtpvFilters = ( let startYear = Math.trunc(Number(filters.startYear ?? currentYear)); let endYear = Math.trunc(Number(filters.endYear ?? startYear)); let elapsedYears = Math.trunc( - Number(filters.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS), + Number( + filters.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS, + ), ); if (!Number.isFinite(startYear)) startYear = currentYear; if (!Number.isFinite(endYear)) endYear = startYear; - if (!Number.isFinite(elapsedYears) || elapsedYears < 1 || elapsedYears > 100) { + if ( + !Number.isFinite(elapsedYears) || + elapsedYears < 1 || + elapsedYears > 100 + ) { elapsedYears = AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS; } if (startYear > endYear) { @@ -85,9 +98,15 @@ export const normalizeAgedPipeStatusByCtpvFilters = ( }; }; -export const validateAgedPipeStatusByCtpvFilters = (filters: AgedPipeStatusByCtpvFilters) => { +export const validateAgedPipeStatusByCtpvFilters = ( + filters: AgedPipeStatusByCtpvFilters, +) => { if (filters.startYear > filters.endYear) return false; - if (filters.endYear - filters.startYear + 1 > AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN) return false; + if ( + filters.endYear - filters.startYear + 1 > + AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN + ) + return false; return filters.elapsedYears >= 1 && filters.elapsedYears <= 100; }; @@ -109,18 +128,25 @@ export const toAgedPipeStatusByCtpvRows = ( }; (row.yearStats ?? []).forEach((yearStat) => { - displayRow[`year${yearStat.year}TotalLengthKm`] = formatMetric(Number(yearStat.totalLengthKm ?? 0)); - displayRow[`year${yearStat.year}AgedLengthKm`] = formatMetric(Number(yearStat.agedLengthKm ?? 0)); - displayRow[`year${yearStat.year}AgedRate`] = formatMetric(Number(yearStat.agedRate ?? 0)); + displayRow[`year${yearStat.year}TotalLengthKm`] = formatMetric( + Number(yearStat.totalLengthKm ?? 0), + ); + displayRow[`year${yearStat.year}AgedLengthKm`] = formatMetric( + Number(yearStat.agedLengthKm ?? 0), + ); + displayRow[`year${yearStat.year}AgedRate`] = formatMetric( + Number(yearStat.agedRate ?? 0), + ); }); return displayRow; }); -export const buildAgedPipeStatusByCtpvHeadList = (years: number[]): UiHeadProps[] => { +export const buildAgedPipeStatusByCtpvHeadList = ( + years: number[], +): UiHeadProps[] => { const heads: UiHeadProps[] = [ - // { id: "rowNumber", title: "NO", widthPercent: 6, sortable: false }, - { id: "ctpvNm", title: "시도", widthPercent: 14, sortable: false }, + { id: "ctpvNm", title: "시도", widthPercent: 14 }, ]; years.forEach((year) => { @@ -129,21 +155,18 @@ export const buildAgedPipeStatusByCtpvHeadList = (years: number[]): UiHeadProps[ id: `year${year}TotalLengthKm`, title: `${year} 전체관로연장(km)`, widthPercent: 13, - sortable: false, align: "right", }, { id: `year${year}AgedLengthKm`, title: `${year} 노후관로연장(km)`, widthPercent: 13, - sortable: false, align: "right", }, { id: `year${year}AgedRate`, title: `${year} 노후화율(%)`, widthPercent: 11, - sortable: false, align: "right", }, ); diff --git a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx index fe16774ba..b060b7564 100644 --- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx +++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx @@ -12,7 +12,10 @@ import { TriangleAlert, Waves, } from "lucide-react"; -import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions"; +import { + windowPopupDefinitions, + type WindowPopupKey, +} from "@/features/windowPopup/model/popupDefinitions"; type Props = { onOpenPopup: (popupKey: WindowPopupKey) => Window | null; @@ -58,23 +61,51 @@ const ANALYSIS_POPUP_ITEMS: Array<{ icon?: ComponentType<{ className?: string }>; renderIcon?: (props: MenuIconProps) => ReactNode; }> = [ - { key: "infoSearch", label: "정보검색", icon: Search }, - { key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig }, - { key: "floodPredictionMapPopup", label: "침수예측현황", icon: Waves }, { - key: "agedPipeStatusByCtpvPopup", - label: "시도별 노후관로현황", + key: "infoSearch", + label: windowPopupDefinitions.mapSearch.title, + icon: Search, + }, + { + key: "pipeAnalysisPopup", + label: windowPopupDefinitions.pipeAnalysisPopup.title, icon: ChartColumnBig, }, - { key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler }, - { key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 }, - { key: "flowRiskPopup", label: "통수 위험", icon: TriangleAlert }, + { + key: "floodPredictionMapPopup", + label: windowPopupDefinitions.floodPredictionMapPopup.title, + icon: Waves, + }, + { + key: "agedPipeStatusByCtpvPopup", + label: windowPopupDefinitions.agedPipeStatusByCtpvPopup.title, + icon: ChartColumnBig, + }, + { + key: "gisEditPopup", + label: windowPopupDefinitions.gisEditPopup.title, + icon: PencilRuler, + }, + { + key: "gisManagementPopup", + label: windowPopupDefinitions.gisManagementPopup.title, + icon: Settings2, + }, + { + key: "flowRiskPopup", + label: windowPopupDefinitions.flowRiskPopup.title, + icon: TriangleAlert, + }, { key: "inftwtResultPopup", - label: "침입수 분석결과", + label: windowPopupDefinitions.inftwtResultPopup.title, renderIcon: InftwtMenuIcon, }, - { key: "rdiiResultPopup", label: "RDII 분석결과", icon: ChartColumnBig }, + { + key: "rdiiResultPopup", + label: windowPopupDefinitions.rdiiResultPopup.title, + icon: ChartColumnBig, + }, ]; export default function AnalysisPopupMenu({ 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 e65222ddf..bd3a44dbe 100644 --- a/apps/sn/app/(map-layout)/map/components/common/table.ts +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -32,7 +32,7 @@ export const MsSwagePplCol = [ ] // 하수관로 searchModal 기본정보tab export const MsSwagePplCol2 = [ - { id: 'rowNumber', title: '일련\n번호' }, + { id: 'rowNumber', title: '순번' }, { id: 'fcltMngNo', title: '시설\n관리번호' }, { id: 'instMngNo', title: '기관\n관리번호' }, { id: 'flowDirectionBasis', title: '유하방향\n근거' }, @@ -398,7 +398,7 @@ export const SwtrmtFcltCol = [ // { id: 'obsrvnSptCd', title: '관측지점코드' }, ]; export const SwtrmtFcltCol2 = [ - { id: 'rowNumber', title: '일련\n번호' }, + { id: 'rowNumber', title: '순번' }, { id: 'swtrmtFcltNm', title: '하수처리\n시설명' }, { id: 'fcltCpct', title: '시설\n용량\n(㎥/d)' }, { id: 'rgnExpln', title: '지역설명' }, @@ -461,7 +461,7 @@ export const SwtrmtPrcsZoneCol = [ { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, ]; export const SwtrmtPrcsZoneCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, // { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, { id: 'swtrmtZoneNm', title: '하수처리구역명' }, // { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' }, @@ -500,7 +500,7 @@ export const FldngWtlvMapCol = [ { id: 'rain_str', title: '강우강도(mm/hr)' }, ]; export const FldngWtlvMapCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, { id: 'fldngYr', title: '침수년도' }, { id: 'fldngDstrNm', title: '침수재해명' }, { id: 'ctpvNm', title: '시도' }, @@ -561,7 +561,7 @@ export const MsSwageMhlCol = [ // { id: 'inst_mng_no', title: '기관관리번호' } ]; export const MsSwageMhlCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, // { id: 'fid', title: 'FID' }, // { id: 'geom', title: 'geometry' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, @@ -683,7 +683,7 @@ export const ExclnSewagePumpCol = [ // { id: 'instMngNo', title: '기관관리번호' }, ] export const ExclnSewagePumpCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, // { id: 'fid', title: 'FID' }, // { id: 'geom', title: 'geometry' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, @@ -739,7 +739,7 @@ export const DrtwSewagePumpCol = [ // { id: 'instMngNo', title: '기관관리번호' }, ] export const DrtwSewagePumpCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, // { id: 'fid', title: 'FID' }, // { id: 'geom', title: 'geometry' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, @@ -788,7 +788,7 @@ export const stofcbrCol = [ { id: 'drtn_angl', title: '방향각도(°)' }, ] export const stofcbrCol2 = [ - { id: 'rowNumber', title: '일련번호' }, + { id: 'rowNumber', title: '순번' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, { id: 'fcltMngNo', title: '시설관리번호' }, // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, diff --git a/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx b/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx index a600c9a2f..8d0907f5a 100644 --- a/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx +++ b/apps/sn/app/(map-layout)/map/components/exmn/ExmnLayerSettingPanel.tsx @@ -23,14 +23,17 @@ export default function ExmnLayerSettingPanel({ }); }; const selectClass = - "h-[24px] w-[54px] rounded-[4px] border border-[#ccd6df] bg-white px-2 text-[1rem] text-[#222] outline-none focus:border-primary"; - const labelClass = "shrink-0 text-[1.05rem] font-medium text-slate-700"; + "h-[24px] w-[54px] rounded-[4px] border border-input bg-background px-2 text-[1rem] text-foreground outline-none focus:border-primary"; + const labelClass = "shrink-0 text-[1.05rem] font-medium text-label"; return ( -
+
- - + {/* 관로/맨홀 조사유형을 구분하는 지도 범례 의미색이므로 고정색을 유지한다. */} + + {jobType === "PPL" ? "관로조사" : "맨홀조사"}
@@ -52,7 +55,9 @@ export default function ExmnLayerSettingPanel({ updateFilter("operationalGrade", event.target.value)} + onChange={(event) => + updateFilter("operationalGrade", event.target.value) + } > {GRADE_OPTIONS.map((grade) => (
{rows.map(([label, value], index) => ( - - @@ -35,105 +39,115 @@ const renderRows = (rows: Array<[string, string]>) => (
+ {label} + {value}
); -export default function FldngHstryDetailPanel({ detail, onClose, position }: Props) { +export default function FldngHstryDetailPanel({ + detail, + onClose, + position, +}: Props) { // 지도에서 침수흔적도 polygon을 클릭했을 때, 상세정보는 업무 의미 기준으로 3개 탭에 나눠 보여준다. const [activeTab, setActiveTab] = useState("year"); const yearRows = useMemo( - () => [ - ["침수심", `${detail.floodDepthM.toFixed(2)} m`], - ["침수면적", formatFloodArea(detail.floodAreaHa)], - ["침수년도", `${detail.floodYear}년`], - ["등급", detail.gradeLabel], - ["침수구역", detail.districtName], - ] as Array<[string, string]>, + () => + [ + ["침수심", `${detail.floodDepthM.toFixed(2)} m`], + ["침수면적", formatFloodArea(detail.floodAreaHa)], + ["침수년도", `${detail.floodYear}년`], + ["등급", detail.gradeLabel], + ["침수구역", detail.districtName], + ] as Array<[string, string]>, [detail], ); const disasterRows = useMemo( - () => [ - ["침수재해명", detail.disasterName], - ["침수면적", formatFloodArea(detail.floodAreaHa)], - ["침수원인상세명", detail.causeDetailName], - ["침수시작년월일", detail.startDate], - ["침수종료년월일", detail.endDate], - ["침수시작시간", detail.startTime], - ] as Array<[string, string]>, + () => + [ + ["침수재해명", detail.disasterName], + ["침수면적", formatFloodArea(detail.floodAreaHa)], + ["침수원인상세명", detail.causeDetailName], + ["침수시작년월일", detail.startDate], + ["침수종료년월일", detail.endDate], + ["침수시작시간", detail.startTime], + ] as Array<[string, string]>, [detail], ); const rainfallRows = useMemo( - () => [ - ["침수시작일자", detail.startDate], - ["침수종료일자", detail.endDate], - ["침수시작시간", detail.startTime], - ["침수종료시간", detail.endTime], - ["침수시간", detail.floodHour], - ["강우시작시간", detail.rainStartTime], - ["강우종료시간", detail.rainEndTime], - ["총강우량(mm)", `${detail.totalRainfallMm.toFixed(0)}`], - ["강우강도(mm/hr)", `${detail.rainfallIntensityMmHr.toFixed(0)}`], - ["침수시간", detail.floodHour], - ["침수원인상세코드", detail.causeDetailCode], - ["피해금액", detail.damageAmount], - ] as Array<[string, string]>, + () => + [ + ["침수시작일자", detail.startDate], + ["침수종료일자", detail.endDate], + ["침수시작시간", detail.startTime], + ["침수종료시간", detail.endTime], + ["침수시간", detail.floodHour], + ["강우시작시간", detail.rainStartTime], + ["강우종료시간", detail.rainEndTime], + ["총강우량(mm)", `${detail.totalRainfallMm.toFixed(0)}`], + ["강우강도(mm/hr)", `${detail.rainfallIntensityMmHr.toFixed(0)}`], + ["침수시간", detail.floodHour], + ["침수원인상세코드", detail.causeDetailCode], + ["피해금액", detail.damageAmount], + ] as Array<[string, string]>, [detail], ); - const currentRows = activeTab === "year" - ? yearRows - : activeTab === "disaster" - ? disasterRows - : rainfallRows; + const currentRows = + activeTab === "year" + ? yearRows + : activeTab === "disaster" + ? disasterRows + : rainfallRows; return (
-
-
상세정보
+
+
+ 상세정보 +
-
+
- {[ - ["year", "년도탭"], - ["disaster", "재해명탭"], - ["rainfall", "침수발생강우탭"], - ].map(([tabId, label]) => { - const isActive = activeTab === tabId; + {[ + ["year", "년도탭"], + ["disaster", "재해명탭"], + ["rainfall", "침수발생강우탭"], + ].map(([tabId, label]) => { + const isActive = activeTab === tabId; - return ( - - ); - })} + return ( + + ); + })}
-
-
+
+
{renderRows(currentRows)}
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 1b3159186..ee4789351 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -5,13 +5,15 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button, Input } from "@common/components"; -import { UiTable } from "@common/business_components/ui/table"; -import type { UiTableSort } from "@common/business_components/ui/table"; +import { StandardPopupLayout } from "@common/business_components"; import { getRawJson } from "@/components/lib/api"; import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types"; +import { useAlertStore } from "@/shared/lib"; import useStore from "@/store/useStore"; import LoadingOverlay from "@/components/ui/LoadingBar"; +import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; +import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; import { FLOOD_HISTORY_LAYER_TYPE, FLDNG_HSTRY_DEFAULT_FILTERS, @@ -44,11 +46,6 @@ const resolveOptionText = ( return options.find((item) => String(item.val).trim() === lookup)?.text ?? ""; }; -const resolveFldngHstryApiSort = (sort?: UiTableSort) => { - if (!sort?.key || !sort.direction) return undefined; - return { key: sort.key, direction: sort.direction } as UiTableSort; -}; - const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => { const fid = String(row.fid ?? "").trim(); if (fid.startsWith(`${FLOOD_HISTORY_LAYER_TYPE}.`)) return fid; @@ -90,16 +87,18 @@ const resolveRegionNames = ( }); export default function FldngHstryPopupPage() { + const alert = useAlertStore((state) => state.setMessage); const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); - const [filters, setFilters] = useState(FLDNG_HSTRY_DEFAULT_FILTERS); - const [appliedFilters, setAppliedFilters] = useState(FLDNG_HSTRY_DEFAULT_FILTERS); + 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); const [isLoading, setIsLoading] = useState(false); - const [sort, setSort] = useState(); - const [appliedSort, setAppliedSort] = useState(); useEffect(() => { if (ctpv.length === 0) void fetchCtpv(); @@ -121,21 +120,27 @@ export default function FldngHstryPopupPage() { () => filters.ctpvCd === "0" ? sgg - : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))), + : sgg.filter((item) => + String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), + ), [filters.ctpvCd, sgg], ); const fetchRows = useCallback( - async (page: number, nextFilters: FldngHstryFilters, size: number, nextSort?: UiTableSort) => { + async (page: number, nextFilters: FldngHstryFilters, size: number) => { const normalized = normalizeFldngHstryFilters(nextFilters); const queryString = buildFldngHstryListQueryString( normalized, page, size, - resolveFldngHstryApiSort(nextSort), ); - const response = await getRawJson(`/sewerage-service/api/v1/fldng/hstry?${queryString}`); - const payload = unwrapPayload<{ items?: FldngHstryApiItem[]; total?: number }>(response); + const response = await getRawJson( + `/sewerage-service/api/v1/fldng/hstry?${queryString}`, + ); + const payload = unwrapPayload<{ + items?: FldngHstryApiItem[]; + total?: number; + }>(response); const items = Array.isArray(payload?.items) ? payload.items : []; const displayItems = items.map((item) => { @@ -156,32 +161,37 @@ export default function FldngHstryPopupPage() { ); const loadRows = useCallback( - async (page: number, nextFilters: FldngHstryFilters, nextSort?: UiTableSort) => { + async (page: number, nextFilters: FldngHstryFilters) => { setIsLoading(true); try { const normalized = normalizeFldngHstryFilters(nextFilters); - const response = await fetchRows(page, normalized, FLDNG_HSTRY_PAGE_SIZE, nextSort); + const response = await fetchRows( + page, + normalized, + FLDNG_HSTRY_PAGE_SIZE, + ); 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 : "침수발생이력 목록 조회 중 오류가 발생했습니다."); + alert( + error instanceof Error + ? error.message + : "침수발생이력 목록 조회 중 오류가 발생했습니다.", + ); } finally { setIsLoading(false); } }, - [fetchRows], + [alert, fetchRows], ); useEffect(() => { - void loadRows(1, FLDNG_HSTRY_DEFAULT_FILTERS, undefined); + void loadRows(1, FLDNG_HSTRY_DEFAULT_FILTERS); // 최초 로딩은 한 번만 수행한다. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -195,24 +205,14 @@ export default function FldngHstryPopupPage() { } setFilters(normalized); - await loadRows(page, normalized, sort); + await loadRows(page, normalized); }; const handlePageChange = async (page: number) => { - await loadRows(page, appliedFilters, appliedSort); + await loadRows(page, appliedFilters); }; - 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); - + const handleRowClick = (row: FldngHstryTableRow) => { const mapFeatureFid = resolveFldngHstryMapFeatureFid(row); if (mapFeatureFid) { window.opener?.postMessage( @@ -250,7 +250,6 @@ export default function FldngHstryPopupPage() { 1, appliedFilters, Math.max(total, FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE), - appliedSort, ); await downloadExcelWithKoreanHeaders({ data: buildDownloadRows(response.rows) as unknown as JSON[], @@ -261,149 +260,155 @@ export default function FldngHstryPopupPage() { }); } catch (error) { console.error("침수발생이력 다운로드 중 오류:", error); - alert(error instanceof Error ? error.message : "침수발생이력 다운로드 중 오류가 발생했습니다."); + alert( + error instanceof Error + ? error.message + : "침수발생이력 다운로드 중 오류가 발생했습니다.", + ); } finally { setIsLoading(false); } }; return ( -
+ -
-

침수발생이력

-
-
-
-
- + + - + - + - - -
- - -
-
- -
- - headName={<>침수발생이력 조회 결과 총 {total.toLocaleString()}건} - tableData={rows} - headlist={FLDNG_HSTRY_LIST_HEADLIST} - pageSize={FLDNG_HSTRY_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)} +
+ -
- 행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다. +
+ +
-
-
+ + + >} + headlist={FLDNG_HSTRY_LIST_HEADLIST} + title="침수발생이력 목록" + total={total} + page={currentPage} + pageSize={FLDNG_HSTRY_PAGE_SIZE} + onPageChange={(page) => void handlePageChange(page)} + onRowClick={handleRowClick} + rowId={(row, index) => String(row.fid ?? index)} + scrollMode="horizontal" + minTableWidth="220rem" + actions={ + + } + /> + +
+ 행을 클릭하면 메인 지도에서 해당 침수흔적을 표시합니다. +
+ ); } 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 8940bdb3d..9c625791f 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/fldngHstryTypes.ts @@ -1,6 +1,6 @@ "use client"; -import type { UiHeadProps, UiTableSort } from "@common/business_components/ui"; +import type { UiHeadProps } from "@common/business_components/ui"; export const FLOOD_HISTORY_POPUP_KEY = "fldngHstryPopup"; export const FLDNG_HSTRY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT"; @@ -119,27 +119,25 @@ export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [ ] as const; export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [ - { id: "rowNo", title: "NO", widthPercent: 5, sortable: false }, - { id: "floodYearDisplay", title: "침수\n년도", 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: "강우\n시작일시", widthPercent: 10, sortable: true, sortKey: "rainBgngTm" }, - { id: "rainEndTmDisplay", title: "강우\n종료일시", 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: "평균\n침수위", widthPercent: 7, sortable: true, sortKey: "avgFldngWtlv" }, - { id: "floodAreaDisplay", title: "침수\n면적\(ha)", widthPercent: 7, sortable: true, sortKey: "fldngArea" }, - { id: "exmnYearDisplay", title: "조사년도", widthPercent: 6, sortable: true, sortKey: "exmnYr" }, - { id: "damageAmountDisplay", title: "피해금액", widthPercent: 9, sortable: true, sortKey: "damAmt" }, + { id: "rowNo", title: "순번", widthPercent: 5 }, + { id: "floodYearDisplay", title: "침수\n년도", widthPercent: 6 }, + { id: "ctpvNm", title: "시도", widthPercent: 7 }, + { id: "sggNm", title: "시군구", widthPercent: 8 }, + { id: "disasterName", title: "침수재해명", widthPercent: 14 }, + { id: "rainBgngTmDisplay", title: "강우\n시작일시", widthPercent: 10 }, + { id: "rainEndTmDisplay", title: "강우\n종료일시", widthPercent: 10 }, + { id: "rainTotDisplay", title: "총강우량(mm)", widthPercent: 8 }, + { id: "rainStrDisplay", title: "강우강도(mm/hr)", widthPercent: 8 }, + { id: "floodHourDisplay", title: "침수시간", widthPercent: 7 }, + { id: "districtName", title: "침수지구명", widthPercent: 10 }, + { id: "avgFloodDepthDisplay", title: "평균\n침수위", widthPercent: 7 }, + { id: "floodAreaDisplay", title: "침수\n면적\(ha)", widthPercent: 7 }, + { id: "exmnYearDisplay", title: "조사년도", widthPercent: 6 }, + { id: "damageAmountDisplay", title: "피해금액", widthPercent: 9 }, ]; export const FLDNG_HSTRY_DOWNLOAD_HEADERS: Record = { - rowNo: "NO", + rowNo: "순번", floodYearDisplay: "침수년도", ctpvNm: "시도", sggNm: "시군구", @@ -183,11 +181,14 @@ const createIrregularRing = ({ return [centerX + jitterX, centerY + jitterY] as [number, number]; }); - return [...points, points[0]]; + return points.length > 0 ? [...points, points[0]!] : []; }; const toNumericSeed = (value: string) => - Array.from(value).reduce((sum, char, index) => sum + char.charCodeAt(0) * (index + 1), 0); + Array.from(value).reduce( + (sum, char, index) => sum + char.charCodeAt(0) * (index + 1), + 0, + ); const buildGeometrySeed = (fid: string) => { const seed = toNumericSeed(fid); @@ -284,7 +285,8 @@ const normalizeHm = (value?: string | null) => { 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)}`; + if (digits.length === 3) + return `0${digits.slice(0, 1)}:${digits.slice(1, 3)}`; return `${digits.slice(0, 2)}:${digits.slice(2, 4)}`; }; @@ -293,7 +295,8 @@ const normalizeDateTime = (value?: string | null) => { if (!source) return ""; if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(source)) return source; const date = new Date(source); - if (Number.isNaN(date.getTime())) return source.slice(0, 16).replace("T", " "); + 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"); @@ -314,8 +317,12 @@ const formatInteger = (value?: number | null, suffix = "") => { return `${numeric.toLocaleString("ko-KR")}${suffix}`; }; -const formatOptionalNumber = (value?: number | null, digits = 1, suffix = "") => { - if (value === null || value === undefined || value === "") return "-"; +const formatOptionalNumber = ( + value?: number | null, + digits = 1, + suffix = "", +) => { + if (value === null || value === undefined) return "-"; const numeric = Number(value); if (!Number.isFinite(numeric)) return "-"; return `${numeric.toFixed(digits)}${suffix}`; @@ -339,7 +346,9 @@ export const normalizeFldngHstryFilters = ( ): FldngHstryFilters => ({ ctpvCd: String(filters.ctpvCd ?? "0") || "0", sggCd: String(filters.sggCd ?? "0") || "0", - floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4), + floodYear: String(filters.floodYear ?? "") + .replace(/[^0-9]/g, "") + .slice(0, 4), disasterName: String(filters.disasterName ?? "").trim(), }); @@ -352,7 +361,6 @@ export const buildFldngHstryListQueryString = ( filters: FldngHstryFilters, page: number, size: number, - sort?: UiTableSort, ) => { const normalized = normalizeFldngHstryFilters(filters); const params = new URLSearchParams({ @@ -360,23 +368,27 @@ export const buildFldngHstryListQueryString = ( 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.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}`); - + if (normalized.disasterName) + params.set("fldngDstrNm", normalized.disasterName); return params.toString(); }; -export const formatFloodArea = (value: number) => Number(value ?? 0).toLocaleString("ko-KR", { +export const formatFloodArea = (value: number) => + Number(value ?? 0).toLocaleString("ko-KR", { minimumFractionDigits: 0, maximumFractionDigits: 2, }); export const formatFloodDepth = (value: number) => value.toFixed(2); -export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryItem => { +export const attachFldngHstryGeometry = ( + item: FldngHstryApiItem, +): FldngHstryItem => { const historyId = String(item.fid ?? ""); const geometrySeed = buildGeometrySeed(historyId); const startDate = normalizeDate(item.bgngYmd); @@ -421,7 +433,11 @@ export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryIte rotationDeg: 9, scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86], }), - depthZones: createFldngHstryDepthZones(historyId, geometrySeed.centerX, geometrySeed.centerY), + depthZones: createFldngHstryDepthZones( + historyId, + geometrySeed.centerX, + geometrySeed.centerY, + ), }; }; @@ -446,20 +462,36 @@ export const toFldngHstryTableRows = ( })); const resolveFldngHstryNodeName = (node: any) => - String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim(); + String( + node?.text ?? + node?.data?.lyrNm ?? + node?.data?.name ?? + node?.lyrNm ?? + node?.name ?? + "", + ).trim(); const resolveFldngHstryLayerType = (node: any) => - String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim(); + String( + node?.data?.lyrTyNm ?? + node?.data?.lyrTypeNm ?? + node?.lyrTyNm ?? + node?.lyrTypeNm ?? + "", + ).trim(); export const isFldngHstryNode = (node: any) => { const name = resolveFldngHstryNodeName(node); const layerType = resolveFldngHstryLayerType(node); - return name === FLOOD_HISTORY_LAYER_NAME - || layerType === FLOOD_HISTORY_LAYER_TYPE - || layerType === FLOOD_HISTORY_PREVIEW_LAYER_TYPE; + return ( + name === FLOOD_HISTORY_LAYER_NAME || + layerType === FLOOD_HISTORY_LAYER_TYPE || + layerType === FLOOD_HISTORY_PREVIEW_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; +export const FLOOD_HISTORY_DOWNLOAD_BATCH_SIZE = + FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE; diff --git a/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx index e60f971e5..b65c7b38f 100644 --- a/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/floodDngrMap/FloodDngrMapPopupPage.tsx @@ -2,11 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@common/components"; -import { UiTable } from "@common/business_components/ui"; -import type { UiTableSort } from "@common/business_components/ui"; +import { StandardPopupLayout } from "@common/business_components"; import { getData } from "@/components/lib/api"; import LoadingOverlay from "@/components/ui/LoadingBar"; import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types"; +import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable"; +import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel"; import useStore from "@/store/useStore"; import { FLOOD_DNGR_MAP_DEFAULT_FILTERS, @@ -30,7 +31,11 @@ const getFloodDngrMapJson = async (url: string) => { let timeoutId: number | undefined; const timeoutPromise = new Promise((_, reject) => { timeoutId = window.setTimeout(() => { - reject(new Error("도시침수지도 조회 시간이 초과되었습니다. 검색조건을 좁혀 다시 조회해주세요.")); + reject( + new Error( + "도시침수지도 조회 시간이 초과되었습니다. 검색조건을 좁혀 다시 조회해주세요.", + ), + ); }, FLOOD_DNGR_MAP_REQUEST_TIMEOUT_MS); }); try { @@ -43,25 +48,38 @@ const getFloodDngrMapJson = async (url: string) => { /** 시설물 레이어의 정보제공포털 도시침수지도 피처를 조건별 목록으로 제공한다. */ export default function FloodDngrMapPopupPage() { const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore(); - const [filters, setFilters] = useState(FLOOD_DNGR_MAP_DEFAULT_FILTERS); - const [appliedFilters, setAppliedFilters] = useState(FLOOD_DNGR_MAP_DEFAULT_FILTERS); + const [filters, setFilters] = useState( + FLOOD_DNGR_MAP_DEFAULT_FILTERS, + ); + const [appliedFilters, setAppliedFilters] = useState( + FLOOD_DNGR_MAP_DEFAULT_FILTERS, + ); const [items, setItems] = useState([]); - const [administrativeScopeCodes, setAdministrativeScopeCodes] = useState(null); + const [administrativeScopeCodes, setAdministrativeScopeCodes] = useState< + string[] | null + >(null); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); - const [selectedIndex, setSelectedIndex] = useState(-1); - const [sort, setSort] = useState(); - const [appliedSort, setAppliedSort] = useState(); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const availableCtpvCodes = useMemo( - () => new Set((administrativeScopeCodes ?? []).map((code) => code.slice(0, 2)).filter(Boolean)), + () => + new Set( + (administrativeScopeCodes ?? []) + .map((code) => code.slice(0, 2)) + .filter(Boolean), + ), [administrativeScopeCodes], ); const availableSggCodes = useMemo( - () => new Set((administrativeScopeCodes ?? []).map((code) => code.slice(0, 5)).filter(Boolean)), + () => + new Set( + (administrativeScopeCodes ?? []) + .map((code) => code.slice(0, 5)) + .filter(Boolean), + ), [administrativeScopeCodes], ); @@ -69,7 +87,9 @@ export default function FloodDngrMapPopupPage() { () => administrativeScopeCodes === null ? ctpv - : ctpv.filter((item) => availableCtpvCodes.has(String(item.val).trim().slice(0, 2))), + : ctpv.filter((item) => + availableCtpvCodes.has(String(item.val).trim().slice(0, 2)), + ), [administrativeScopeCodes, availableCtpvCodes, ctpv], ); @@ -77,10 +97,14 @@ export default function FloodDngrMapPopupPage() { const scopedOptions = administrativeScopeCodes === null ? sgg - : sgg.filter((item) => availableSggCodes.has(String(item.val).trim().slice(0, 5))); + : sgg.filter((item) => + availableSggCodes.has(String(item.val).trim().slice(0, 5)), + ); return filters.ctpvCd === "0" ? scopedOptions - : scopedOptions.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))); + : scopedOptions.filter((item) => + String(item.val).startsWith(filters.ctpvCd.slice(0, 2)), + ); }, [administrativeScopeCodes, availableSggCodes, filters.ctpvCd, sgg]); const rows = useMemo( @@ -92,9 +116,8 @@ export default function FloodDngrMapPopupPage() { ctpv, sgg, total, - sort, ), - [ctpv, currentPage, items, sgg, sort, total], + [ctpv, currentPage, items, sgg, total], ); useEffect(() => { @@ -107,16 +130,18 @@ export default function FloodDngrMapPopupPage() { if (!isLoading) return; const watchdogId = window.setTimeout(() => { setIsLoading(false); - setErrorMessage((current) => - current || "도시침수지도 조회가 지연되어 로딩을 종료했습니다. 검색조건을 좁혀 다시 조회해주세요.", + setErrorMessage( + (current) => + current || + "도시침수지도 조회가 지연되어 로딩을 종료했습니다. 검색조건을 좁혀 다시 조회해주세요.", ); }, FLOOD_DNGR_MAP_LOADING_WATCHDOG_MS); return () => window.clearTimeout(watchdogId); }, [isLoading]); - /** 마지막 적용 조건과 정렬로 서버 페이지를 조회한다. */ + /** 마지막 적용 조건으로 서버 페이지를 조회한다. */ const loadRows = useCallback( - async (page: number, nextFilters: FloodDngrMapFilters, nextSort?: UiTableSort) => { + async (page: number, nextFilters: FloodDngrMapFilters) => { setIsLoading(true); setErrorMessage(""); try { @@ -124,7 +149,6 @@ export default function FloodDngrMapPopupPage() { nextFilters, page - 1, FLOOD_DNGR_MAP_PAGE_SIZE, - nextSort, ); const response = await getFloodDngrMapJson( `/sewerage-service/api/v1/fldng/flood-dngr-map?${queryString}`, @@ -134,21 +158,22 @@ export default function FloodDngrMapPopupPage() { total?: number; } | null; if (!payload || !Array.isArray(payload.items)) { - throw new Error("도시침수지도 목록 API 응답 형식이 올바르지 않습니다."); + throw new Error( + "도시침수지도 목록 API 응답 형식이 올바르지 않습니다.", + ); } setItems(payload.items); setTotal(Number(payload?.total ?? 0)); setCurrentPage(page); setAppliedFilters(nextFilters); - setSort(nextSort); - setAppliedSort(nextSort); - setSelectedIndex(-1); } catch (error) { console.error("도시침수지도 목록 조회 중 오류:", error); setItems([]); setTotal(0); setErrorMessage( - error instanceof Error ? error.message : "도시침수지도 목록 조회 중 오류가 발생했습니다.", + error instanceof Error + ? error.message + : "도시침수지도 목록 조회 중 오류가 발생했습니다.", ); } finally { setIsLoading(false); @@ -164,7 +189,9 @@ export default function FloodDngrMapPopupPage() { "/sewerage-service/api/v1/fldng/flood-dngr-map/scope", ); if (!Array.isArray(response)) { - throw new Error("도시침수지도 행정구역 API 응답 형식이 올바르지 않습니다."); + throw new Error( + "도시침수지도 행정구역 API 응답 형식이 올바르지 않습니다.", + ); } setAdministrativeScopeCodes(response); } catch (error) { @@ -179,15 +206,12 @@ export default function FloodDngrMapPopupPage() { }, [loadAdministrativeScope, loadRows]); const handleSearch = () => { - void loadRows(1, filters, sort); + void loadRows(1, filters); }; /** 시군구·침수빈도 그룹의 범위와 선택 조건을 부모 지도에 전달한다. */ - const handleRowClick = async (index?: number) => { - if (typeof index !== "number") return; - const row = rows[index]; + const handleRowClick = async (row: FloodDngrMapTableRow) => { if (!row?.sggCd || !row?.sbmrgFrqExpln) return; - setSelectedIndex(index); setIsLoading(true); setErrorMessage(""); try { @@ -199,8 +223,16 @@ export default function FloodDngrMapPopupPage() { `/sewerage-service/api/v1/fldng/flood-dngr-map/group/extent?${params.toString()}`, ); const payload = response as FloodDngrMapExtent; - const extent = [payload?.minX, payload?.minY, payload?.maxX, payload?.maxY].map(Number); - if (extent.length !== 4 || extent.some((value) => !Number.isFinite(value))) { + const extent = [ + payload?.minX, + payload?.minY, + payload?.maxX, + payload?.maxY, + ].map(Number); + if ( + extent.length !== 4 || + extent.some((value) => !Number.isFinite(value)) + ) { throw new Error("해당 도시침수지도 피처의 위치 정보가 없습니다."); } window.opener?.postMessage( @@ -229,105 +261,138 @@ export default function FloodDngrMapPopupPage() { }; return ( -
+ -
-

도시침수지도 목록

-
- -
-
- - - - - - - - -
- + +
- - {errorMessage ? ( -
- {errorMessage} -
- ) : null} - -
- - headName={ - <> - 도시침수지도 조회 결과 총 {total.toLocaleString()}건 - + + + + + + + + + +
+ +
-
-
+ + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + + >} + headlist={FLOOD_DNGR_MAP_HEADLIST} + title="도시침수지도 목록" + total={total} + page={currentPage} + pageSize={FLOOD_DNGR_MAP_PAGE_SIZE} + onPageChange={(page) => void loadRows(page, appliedFilters)} + onRowClick={(row) => void handleRowClick(row)} + rowId={(row, index) => `${row.sggCd}-${row.sbmrgFrqExpln}-${index}`} + scrollMode="horizontal" + minTableWidth="120rem" + /> + ); } diff --git a/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts b/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts index 24a4187d7..fd5a335b1 100644 --- a/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts +++ b/apps/sn/app/(map-layout)/map/components/floodDngrMap/floodDngrMapTypes.ts @@ -1,6 +1,6 @@ "use client"; -import type { UiHeadProps, UiTableSort } from "@common/business_components/ui"; +import type { UiHeadProps } from "@common/business_components/ui"; export const FLOOD_DNGR_MAP_POPUP_KEY = "floodDngrMapPopup"; export const FLOOD_DNGR_MAP_LAYER_TYPE = "tb_ms_flood_dngr_map"; @@ -59,47 +59,46 @@ export const FLOOD_DNGR_MAP_DEPTH_LABELS: Record = { N334: "5.0m 이상", }; -export const FLOOD_DNGR_MAP_DEPTH_OPTIONS = Object.entries(FLOOD_DNGR_MAP_DEPTH_LABELS).map( - ([value, label]) => ({ value, label }), -); +export const FLOOD_DNGR_MAP_DEPTH_OPTIONS = Object.entries( + FLOOD_DNGR_MAP_DEPTH_LABELS, +).map(([value, label]) => ({ value, label })); export const FLOOD_DNGR_MAP_HEADLIST: UiHeadProps[] = [ - { id: "rowNo", title: "NO", widthPercent: 8, sortable: true, sortKey: "rowNo" }, - { id: "ctpvNm", title: "시도", widthPercent: 18, sortable: true, sortKey: "ctpvCd" }, - { id: "sggNm", title: "시군구", widthPercent: 20, sortable: true, sortKey: "sggCd" }, + { id: "rowNo", title: "순번", widthPercent: 8 }, + { id: "ctpvNm", title: "시도", widthPercent: 18 }, + { id: "sggNm", title: "시군구", widthPercent: 20 }, { id: "sbmrgDpthlDisplay", title: "침수심도(최소~최대)", widthPercent: 31, - sortable: true, - sortKey: "sbmrgDpthlCd", }, { id: "sbmrgFrqDisplay", title: "침수빈도", widthPercent: 23, - sortable: true, - sortKey: "sbmrgFrqExpln", }, ]; -/** API가 허용하는 검색 조건과 페이징·정렬 파라미터만 쿼리 문자열로 만든다. */ +/** API가 허용하는 검색 조건과 페이징 파라미터만 쿼리 문자열로 만든다. */ export const buildFloodDngrMapListQueryString = ( filters: FloodDngrMapFilters, page: number, size: number, - sort?: UiTableSort, ) => { - const params = new URLSearchParams({ page: String(page), size: String(size) }); - if (filters.ctpvCd && filters.ctpvCd !== "0") params.set("ctpvCd", filters.ctpvCd.slice(0, 2)); - if (filters.sggCd && filters.sggCd !== "0") params.set("sggCd", filters.sggCd.slice(0, 5)); + const params = new URLSearchParams({ + page: String(page), + size: String(size), + }); + if (filters.ctpvCd && filters.ctpvCd !== "0") + params.set("ctpvCd", filters.ctpvCd.slice(0, 2)); + if (filters.sggCd && filters.sggCd !== "0") + params.set("sggCd", filters.sggCd.slice(0, 5)); if (filters.sbmrgDpthlCd && filters.sbmrgDpthlCd !== "0") { params.set("sbmrgDpthlCd", filters.sbmrgDpthlCd); } if (filters.sbmrgFrqExpln && filters.sbmrgFrqExpln !== "0") { params.set("sbmrgFrqExpln", filters.sbmrgFrqExpln); } - if (sort?.key && sort.direction) params.set("sort", `${sort.key},${sort.direction}`); return params.toString(); }; @@ -108,9 +107,14 @@ const resolveAdministrativeName = ( options: AdministrativeOption[], codeLength: number, ) => { - const code = String(value ?? "").trim().slice(0, codeLength); + const code = String(value ?? "") + .trim() + .slice(0, codeLength); const matched = options.find( - (option) => String(option.val ?? "").trim().slice(0, codeLength) === code, + (option) => + String(option.val ?? "") + .trim() + .slice(0, codeLength) === code, ); return String(matched?.text ?? "").trim() || "-"; }; @@ -122,7 +126,10 @@ const toFrequencyDisplay = (value: unknown) => { return Number.isFinite(numericValue) ? `${numericValue}년` : normalized; }; -const FLOOD_DNGR_MAP_DEPTH_BOUNDS: Record = { +const FLOOD_DNGR_MAP_DEPTH_BOUNDS: Record< + string, + { min: string; max: string; openEnded?: boolean } +> = { N330: { min: "0.0", max: "0.5" }, N331: { min: "0.5", max: "1.0" }, N332: { min: "1.0", max: "2.0" }, @@ -133,7 +140,8 @@ const FLOOD_DNGR_MAP_DEPTH_BOUNDS: Record { if (!minCode && !maxCode) return "-"; - if (minCode === maxCode) return FLOOD_DNGR_MAP_DEPTH_LABELS[minCode] || minCode || "-"; + if (minCode === maxCode) + return FLOOD_DNGR_MAP_DEPTH_LABELS[minCode] || minCode || "-"; const minDepth = FLOOD_DNGR_MAP_DEPTH_BOUNDS[minCode]; const maxDepth = FLOOD_DNGR_MAP_DEPTH_BOUNDS[maxCode]; if (!minDepth || !maxDepth) return `${minCode || "-"} ~ ${maxCode || "-"}`; @@ -150,23 +158,30 @@ export const toFloodDngrMapTableRows = ( ctpvOptions: AdministrativeOption[] = [], sggOptions: AdministrativeOption[] = [], total = items.length, - sort?: UiTableSort, ): FloodDngrMapTableRow[] => items.map((item, index) => ({ ...item, - rowNo: - sort?.key === "rowNo" && sort.direction === "desc" - ? total - page * pageSize - index - : page * pageSize + index + 1, + rowNo: total - page * pageSize - index, ctpvNm: resolveAdministrativeName(item.ctpvCd, ctpvOptions, 2), sggNm: resolveAdministrativeName(item.sggCd, sggOptions, 5), - sbmrgDpthlDisplay: toDepthRangeDisplay(item.minSbmrgDpthlCd, item.maxSbmrgDpthlCd), + sbmrgDpthlDisplay: toDepthRangeDisplay( + item.minSbmrgDpthlCd, + item.maxSbmrgDpthlCd, + ), sbmrgFrqDisplay: toFrequencyDisplay(item.sbmrgFrqExpln), })); const resolveNodeName = (node: any) => - String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim(); + String( + node?.text ?? + node?.data?.lyrNm ?? + node?.data?.name ?? + node?.lyrNm ?? + node?.name ?? + "", + ).trim(); /** 빈도·침수심 자식 노드가 아니라 도시침수지도 루트만 목록보기 대상으로 판별한다. */ export const isFloodDngrMapRootNode = (node: any) => - resolveNodeName(node).replace(/\s/g, "") === FLOOD_DNGR_MAP_LAYER_NAME.replace(/\s/g, ""); + resolveNodeName(node).replace(/\s/g, "") === + FLOOD_DNGR_MAP_LAYER_NAME.replace(/\s/g, ""); diff --git a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx index 591077005..2591591c6 100644 --- a/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/gisEdit/GisEditPipeSessionPopup.tsx @@ -25,9 +25,27 @@ type MngInstOption = { const EMPTY_SELECT_OPTION = { value: "", label: "선택" }; const PIPE_BASIC_INFO_FIELDS = [ - { id: "mngInstCd", sourceId: "mng_inst_cd", title: "관리기관", type: "select", required: true }, - { id: "fcltMngNo", sourceId: "fclt_mng_no", title: "시설관리번호", type: "input", required: true }, - { id: "instMngNo", sourceId: "inst_mng_no", title: "기관관리번호", type: "input", required: true }, + { + id: "mngInstCd", + sourceId: "mng_inst_cd", + title: "관리기관", + type: "select", + required: true, + }, + { + id: "fcltMngNo", + sourceId: "fclt_mng_no", + title: "시설관리번호", + type: "input", + required: true, + }, + { + id: "instMngNo", + sourceId: "inst_mng_no", + title: "기관관리번호", + type: "input", + required: true, + }, { id: "instlYmd", sourceId: "instl_ymd", title: "설치일자", type: "date" }, { id: "mapshtNo", sourceId: "mapsht_no", title: "도엽번호", type: "input" }, { id: "pdmtVl", sourceId: "pdmt_vl", title: "관경(mm)", type: "input" }, @@ -40,7 +58,10 @@ const PIPE_BASIC_INFO_FIELDS = [ required: true, options: [ EMPTY_SELECT_OPTION, - ...MS_SWAGE_PPL_USE_CODES.map((item) => ({ value: item.val.trim(), label: item.text })), + ...MS_SWAGE_PPL_USE_CODES.map((item) => ({ + value: item.val.trim(), + label: item.text, + })), ], }, { @@ -50,33 +71,95 @@ const PIPE_BASIC_INFO_FIELDS = [ type: "select", options: [ EMPTY_SELECT_OPTION, - ...MS_SWAGE_PPL_CODES.map((item) => ({ value: item.val.trim(), label: item.text })), + ...MS_SWAGE_PPL_CODES.map((item) => ({ + value: item.val.trim(), + label: item.text, + })), ], }, - { id: "avgGrdn", sourceId: "avg_grdn", title: "평균구배(m/m)", type: "input" }, - { id: "drtwMltvArea", sourceId: "drtw_mltv_area", title: "오수배수면적(m²)", type: "input" }, - { id: "exclnMltvArea", sourceId: "excln_mltv_area", title: "우수배수면적(m²)", type: "input" }, - { id: "tmptDpth", sourceId: "tmpt_dpth", title: "시점깊이(m)", type: "input" }, - { id: "tmptMhlMngNo", sourceId: "tmpt_mhl_mng_no", title: "시점맨홀관리번호", type: "input" }, - { id: "scendDpth", sourceId: "scend_dpth", title: "종점깊이(m)", type: "input" }, - { id: "scendMhlMngNo", sourceId: "scend_mhl_mng_no", title: "종점맨홀관리번호", type: "input" }, + { + id: "avgGrdn", + sourceId: "avg_grdn", + title: "평균구배(m/m)", + type: "input", + }, + { + id: "drtwMltvArea", + sourceId: "drtw_mltv_area", + title: "오수배수면적(m²)", + type: "input", + }, + { + id: "exclnMltvArea", + sourceId: "excln_mltv_area", + title: "우수배수면적(m²)", + type: "input", + }, + { + id: "tmptDpth", + sourceId: "tmpt_dpth", + title: "시점깊이(m)", + type: "input", + }, + { + id: "tmptMhlMngNo", + sourceId: "tmpt_mhl_mng_no", + title: "시점맨홀관리번호", + type: "input", + }, + { + id: "scendDpth", + sourceId: "scend_dpth", + title: "종점깊이(m)", + type: "input", + }, + { + id: "scendMhlMngNo", + sourceId: "scend_mhl_mng_no", + title: "종점맨홀관리번호", + type: "input", + }, { id: "trflnCnt", sourceId: "trfln_cnt", title: "차선수", type: "input" }, { id: "vrtcLen", sourceId: "vrtc_len", title: "세로길이(m)", type: "input" }, { id: "wdthLen", sourceId: "wdth_len", title: "가로길이(m)", type: "input" }, ]; const PIPE_INPUT_INFO_FIELDS = [ - { id: "instlMbdyCd", title: "설치주체", type: "select", options: [EMPTY_SELECT_OPTION, "BTL", "재정사업"] }, + { + id: "instlMbdyCd", + title: "설치주체", + type: "select", + options: [EMPTY_SELECT_OPTION, "BTL", "재정사업"], + }, { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, { id: "mltvZoneNm", title: "배수구역", type: "input" }, { id: "mltvDtdvNm", title: "배수분구", type: "input" }, - { id: "lineCd", title: "지선/간선", type: "select", options: [EMPTY_SELECT_OPTION, "지선", "간선"] }, - { id: "floCd", title: "흐름방식", type: "select", options: [EMPTY_SELECT_OPTION, "자연유하", "압송"] }, - { id: "layCd", title: "매설위치", type: "select", options: [EMPTY_SELECT_OPTION, "도로", "하상"] }, + { + id: "lineCd", + title: "지선/간선", + type: "select", + options: [EMPTY_SELECT_OPTION, "지선", "간선"], + }, + { + id: "floCd", + title: "흐름방식", + type: "select", + options: [EMPTY_SELECT_OPTION, "자연유하", "압송"], + }, + { + id: "layCd", + title: "매설위치", + type: "select", + options: [EMPTY_SELECT_OPTION, "도로", "하상"], + }, ]; -const PIPE_VISIBLE_BASIC_FIELD_IDS = PIPE_BASIC_INFO_FIELDS.map((field) => field.id); -const PIPE_VISIBLE_INPUT_FIELD_IDS = PIPE_INPUT_INFO_FIELDS.map((field) => field.id); +const PIPE_VISIBLE_BASIC_FIELD_IDS = PIPE_BASIC_INFO_FIELDS.map( + (field) => field.id, +); +const PIPE_VISIBLE_INPUT_FIELD_IDS = PIPE_INPUT_INFO_FIELDS.map( + (field) => field.id, +); const toEditableText = (value: unknown) => { const text = String(value ?? "").trim(); @@ -84,7 +167,10 @@ const toEditableText = (value: unknown) => { }; const isInvalidPipeLookupKey = (value: string) => - !value || value === "-" || /^NEW-/i.test(value) || /^(null|undefined)$/i.test(value); + !value || + value === "-" || + /^NEW-/i.test(value) || + /^(null|undefined)$/i.test(value); const toDateInputValue = (value: unknown) => { const text = toEditableText(value).replaceAll(".", "").replaceAll("-", ""); @@ -92,10 +178,13 @@ const toDateInputValue = (value: unknown) => { return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`; }; -const toDbYmdValue = (value: unknown) => toEditableText(value).replaceAll("-", ""); +const toDbYmdValue = (value: unknown) => + toEditableText(value).replaceAll("-", ""); -const getObjectValue = (values: Record, field: { id: string; sourceId?: string }) => - values[field.id] ?? (field.sourceId ? values[field.sourceId] : undefined); +const getObjectValue = ( + values: Record, + field: { id: string; sourceId?: string }, +) => values[field.id] ?? (field.sourceId ? values[field.sourceId] : undefined); const pickFields = (values: Record, fieldIds: string[]) => fieldIds.reduce>((acc, fieldId) => { @@ -116,13 +205,17 @@ const normalizeBasicFormValues = (values: Record) => { const buildBaseFeatureValues = (featureData: any) => { const resolveUsageCode = (value: unknown) => { const raw = String(value ?? "").trim(); - const matched = MS_SWAGE_PPL_USE_CODES.find((item) => item.val === raw || item.text === raw); + const matched = MS_SWAGE_PPL_USE_CODES.find( + (item) => item.val === raw || item.text === raw, + ); return matched?.val.trim() ?? raw; }; const resolveMaterialCode = (value: unknown) => { const raw = String(value ?? "").trim(); - const matched = MS_SWAGE_PPL_CODES.find((item) => item.val === raw || item.text === raw); + const matched = MS_SWAGE_PPL_CODES.find( + (item) => item.val === raw || item.text === raw, + ); return matched?.val.trim() ?? raw; }; @@ -149,7 +242,11 @@ const buildBaseFeatureValues = (featureData: any) => { const getPipeUsageLabel = (value: unknown) => { const raw = String(value ?? "").trim(); if (!raw) return ""; - return MS_SWAGE_PPL_USE_CODES.find((item) => item.val.trim() === raw || item.text === raw)?.text ?? raw; + return ( + MS_SWAGE_PPL_USE_CODES.find( + (item) => item.val.trim() === raw || item.text === raw, + )?.text ?? raw + ); }; const buildInitialInputDraft = (change: GisEditChange) => ({ @@ -169,11 +266,16 @@ export default function GisEditPipeSessionPopup({ onSave, }: Props) { const isNewPipe = change.editType === "ADD"; - const baseFeatureValues = useMemo(() => buildBaseFeatureValues(featureData), [featureData]); - const [basicInfoTableData, setBasicInfoTableData] = useState>({}); - const [inputInfoTableData, setInputInfoTableData] = useState>( - buildInitialInputDraft(change), + const baseFeatureValues = useMemo( + () => buildBaseFeatureValues(featureData), + [featureData], ); + const [basicInfoTableData, setBasicInfoTableData] = useState< + Record + >({}); + const [inputInfoTableData, setInputInfoTableData] = useState< + Record + >(buildInitialInputDraft(change)); const [mngInstOptions, setMngInstOptions] = useState([]); useEffect(() => { @@ -195,24 +297,21 @@ export default function GisEditPipeSessionPopup({ }; }, []); - const mngInstSelectOptions = useMemo( - () => { - const sortedOptions = [...mngInstOptions].sort((a, b) => - String(a.lwrInstNm ?? a.mngInstCd ?? "").localeCompare( - String(b.lwrInstNm ?? b.mngInstCd ?? ""), - "ko", - ), - ); - return [ - EMPTY_SELECT_OPTION, - ...sortedOptions.map((option) => ({ - value: String(option.mngInstCd ?? "").trim(), - label: option.lwrInstNm || String(option.mngInstCd ?? "").trim(), - })), - ]; - }, - [mngInstOptions], - ); + const mngInstSelectOptions = useMemo(() => { + const sortedOptions = [...mngInstOptions].sort((a, b) => + String(a.lwrInstNm ?? a.mngInstCd ?? "").localeCompare( + String(b.lwrInstNm ?? b.mngInstCd ?? ""), + "ko", + ), + ); + return [ + EMPTY_SELECT_OPTION, + ...sortedOptions.map((option) => ({ + value: String(option.mngInstCd ?? "").trim(), + label: option.lwrInstNm || String(option.mngInstCd ?? "").trim(), + })), + ]; + }, [mngInstOptions]); const basicInfoFields = useMemo( () => @@ -239,14 +338,17 @@ export default function GisEditPipeSessionPopup({ const fcltMngNo = String(change.featureRef?.fcltMngNo ?? "").trim(); const mngInstCd = String(change.featureRef?.mngInstCd ?? "").trim(); - if (isInvalidPipeLookupKey(fcltMngNo) || isInvalidPipeLookupKey(mngInstCd)) return; + if (isInvalidPipeLookupKey(fcltMngNo) || isInvalidPipeLookupKey(mngInstCd)) + return; let isMounted = true; const fetchPipeInputInfo = async () => { try { const safeFcltMngNo = encodeURIComponent(fcltMngNo); const safeMngInstCd = encodeURIComponent(mngInstCd); - const result = await getData(`v1/fcltppl/${safeFcltMngNo}/${safeMngInstCd}`); + const result = await getData( + `v1/fcltppl/${safeFcltMngNo}/${safeMngInstCd}`, + ); if (!isMounted || !result) return; setInputInfoTableData((previous) => ({ @@ -263,14 +365,25 @@ export default function GisEditPipeSessionPopup({ return () => { isMounted = false; }; - }, [change.changeId, change.featureRef?.fcltMngNo, change.featureRef?.mngInstCd, isNewPipe]); + }, [ + change.changeId, + change.featureRef?.fcltMngNo, + change.featureRef?.mngInstCd, + isNewPipe, + ]); const buildMergedDraft = ( nextBasicInfoTableData: Record, nextInputInfoTableData: Record, ): GisEditPipeBasicInfo => { - const basicDraft = pickFields(nextBasicInfoTableData, PIPE_VISIBLE_BASIC_FIELD_IDS); - const inputDraft = pickFields(nextInputInfoTableData, PIPE_VISIBLE_INPUT_FIELD_IDS); + const basicDraft = pickFields( + nextBasicInfoTableData, + PIPE_VISIBLE_BASIC_FIELD_IDS, + ); + const inputDraft = pickFields( + nextInputInfoTableData, + PIPE_VISIBLE_INPUT_FIELD_IDS, + ); if (basicDraft.instlYmd) { basicDraft.instlYmd = toDbYmdValue(basicDraft.instlYmd); } @@ -290,29 +403,43 @@ export default function GisEditPipeSessionPopup({ onSave(change.changeId, buildMergedDraft(basicInfoTableData, nextFormData)); }; - const basicInfoTableRows = useMemo(() => [basicInfoTableData], [basicInfoTableData]); - const inputInfoTableRows = useMemo(() => [inputInfoTableData], [inputInfoTableData]); - const displayManagementNo = toEditableText(change.featureRef?.fcltMngNo ?? change.objectId ?? ""); + const basicInfoTableRows = useMemo( + () => [basicInfoTableData], + [basicInfoTableData], + ); + const inputInfoTableRows = useMemo( + () => [inputInfoTableData], + [inputInfoTableData], + ); + const displayManagementNo = toEditableText( + change.featureRef?.fcltMngNo ?? change.objectId ?? "", + ); return ( -