From 3b170b09924fb72b9ecd07ae1e956df84b4905a5 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 28 Apr 2026 17:08:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[st]=20feat:=20=EC=9E=85=EB=A0=A5-=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5,=EC=A0=9C=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(01,02)=20&=2002-6tab=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=A7=91=EA=B3=84-=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9A=94=EC=95=BD=ED=91=9C?= =?UTF-8?q?=20=ED=8F=AC=ED=95=A8=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/css/DataGrid.css | 18 +- .../src/components/module/CompactDataGrid.tsx | 60 ++ apps/st/src/components/module/DataGrid.tsx | 152 +++-- .../components/module/GridTopActionButton.tsx | 13 +- apps/st/src/components/module/SearchForm.tsx | 31 +- .../module/SidoSggFacilityNameSearchForm.tsx | 245 ++++++- .../statistics/input/SewagePipeInp.tsx | 642 +++++++++++++----- .../input/SeweragePenetrationRateInp.tsx | 59 +- .../statistics/management/LogHistory.tsx | 1 - .../statistics/total/CellColorInfo.tsx | 22 + .../total/SewagePipeMaintainTot.tsx | 198 +++++- .../statistics/total/SewagePipeTot.tsx | 162 ++++- .../total/SeweragePenetrationRateTot.tsx | 15 +- .../total/SewerageServiceLevelTot.tsx | 198 ++++-- .../generalVerification/DetailComponent.tsx | 1 - apps/st/src/components/type/DataGrid.type.ts | 154 +++-- .../st/src/components/type/SearchForm.type.ts | 3 + .../components/util/column/input/DoList.ts | 11 + .../sewagePipe/ConnectionStatusInpColumn.ts | 12 +- .../sewagePipe/FactInstallStatusInpColumn.ts | 4 +- .../util/column/total/SewagePipeTotColumn.ts | 35 +- .../PipeDredgingStatusTotColumn.ts | 79 +++ .../RepairStatusTotColumn.ts | 112 +++ .../sewagePipeMaintainColumn.ts | 12 + .../statistics-sewage-pipe-input-service.ts | 30 + .../api/statistics-total-service.ts | 67 +- .../statistics-sewage-pipe-input-interface.ts | 107 ++- ...tics-sewage-supply-rate-input-interface.ts | 54 +- ...cs-sewage-pipe-maintain-total-interface.ts | 77 +++ .../statistics-sewage-pipe-total-interface.ts | 177 +++++ ...tics-sewage-supply-rate-total-interface.ts | 2 + 31 files changed, 2267 insertions(+), 486 deletions(-) create mode 100644 apps/st/src/components/statistics/total/CellColorInfo.tsx create mode 100644 apps/st/src/components/util/column/total/sewagePipeMaintain/PipeDredgingStatusTotColumn.ts create mode 100644 apps/st/src/components/util/column/total/sewagePipeMaintain/RepairStatusTotColumn.ts create mode 100644 apps/st/src/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index c1b346ad5..2d18921f9 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -35,6 +35,9 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum { @apply !bg-[#ffe9cf]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-summary { + @apply !bg-[#feee85]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-other { @apply !bg-[#fff7bf]; } @@ -42,6 +45,9 @@ .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-biz-sum { @apply !bg-[#ffe9cf]; } + .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-summary { + @apply !bg-[#feee85]; + } .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-other { @apply !bg-[#fff7bf]; } @@ -49,6 +55,10 @@ .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum { @apply !bg-[#ffe9cf]; } + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-summary, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-summary { + @apply !bg-[#feee85]; + } .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-other, .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other { @apply !bg-[#fff7bf]; @@ -56,13 +66,13 @@ /* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */ .data-grid-status-bubble { - @apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5; + @apply rounded-lg border border-slate-200/80 bg-white px-8 py-2.5; box-shadow: none; } - .data-grid-status-bubble span { - @apply text-base text-slate-700; + .data-grid-status-bubble > span { + @apply text-xl text-slate-700 font-bold; } - .data-grid-status-bubble span + span { + .data-grid-status-bubble > span + span { @apply ml-4 border-l border-slate-200/70 pl-4; } diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 048c21cec..47f6c3afd 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -25,6 +25,12 @@ interface RealGridDataProviderComparer { getValue?(row: number, field: string | number): unknown; } +type GridViewWithCheckApi = GridViewWithSetColumns & { + checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void; + checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void; + checkAll?: (checked: boolean) => void; +}; + /** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) { return columns.map((col) => { @@ -72,6 +78,8 @@ const CompactDataGrid = ({ getGridDataRef, cellStyleCallback, onRowClick, + onItemChecked, + onItemAllChecked, compactFullWidth = false, compactGridHeightClass = "h-[65vh]", }: DataGridProps) => { @@ -96,6 +104,32 @@ const CompactDataGrid = ({ (dataProvider as { setRows: (r: unknown[]) => void }).setRows(rowData); } + // __checked 플래그(1/true/Y) 기반으로 체크바 상태 동기화 + if (showHeaderCheckbox) { + const checkGrid = gridView as GridViewWithCheckApi; + const checkByRowFlag = (value: unknown) => + value === true || + value === 1 || + value === "1" || + value === "Y" || + value === "y" || + value === "true"; + try { + checkGrid.checkAll?.(false); + rowData.forEach((row, itemIndex) => { + const checked = checkByRowFlag((row as Record).__checked); + if (!checked) return; + if (checkGrid.checkItem) { + checkGrid.checkItem(itemIndex, true, false); + return; + } + checkGrid.checkRow?.(itemIndex, true, false); + }); + } catch (e) { + console.warn("check sync:", e); + } + } + // setDataComparer로 숫자 정렬 (setFields 미사용 → 데이터 유실·필드 중복 오류 없음) const dp = dataProvider as RealGridDataProviderComparer; if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") { @@ -337,6 +371,32 @@ const CompactDataGrid = ({ }; }, [onRowClick]); + useEffect(() => { + const gridView = gridRef.current?.gridView; + if (!gridView) return; + + if (onItemChecked) { + gridView.onItemChecked = (_grid, itemIndex, checked) => { + onItemChecked(itemIndex, checked); + }; + } else { + gridView.onItemChecked = undefined; + } + + if (onItemAllChecked) { + gridView.onItemAllChecked = (_grid, checked) => { + onItemAllChecked(checked); + }; + } else { + gridView.onItemAllChecked = undefined; + } + + return () => { + if (gridView.onItemChecked) gridView.onItemChecked = undefined; + if (gridView.onItemAllChecked) gridView.onItemAllChecked = undefined; + }; + }, [onItemChecked, onItemAllChecked]); + return (
diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 93e5fccc5..82a9313d0 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -580,10 +580,10 @@ const DataGrid = ({ ) { const cell = worksheet[ - XLSX.utils.encode_cell({ - r: rowIndex, - c: col, - }) + XLSX.utils.encode_cell({ + r: rowIndex, + c: col, + }) ]; if ( cell && @@ -659,7 +659,7 @@ const DataGrid = ({ const col = range.s.c + colIndex; const cell = worksheet[ - XLSX.utils.encode_cell({ r: row, c: col }) + XLSX.utils.encode_cell({ r: row, c: col }) ]; const headerName = headers[colIndex] || `Column${colIndex + 1}`; @@ -783,13 +783,13 @@ const DataGrid = ({ onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean; } ).onSorting = enableSort - ? createOnSortingHandler( - columns, - setRowData, - sortDirRef, - originalRowsRef, - ) - : () => false; + ? createOnSortingHandler( + columns, + setRowData, + sortDirRef, + originalRowsRef, + ) + : () => false; (gridView.displayOptions as DisplayOptions).fitStyle = GridFitStyle.EVEN; @@ -959,7 +959,7 @@ const DataGrid = ({ ) { ( column.footer[ - index + index ] as FooterItemWithCallback ).valueCallback = footerItem.valueCallback; @@ -1012,11 +1012,11 @@ const DataGrid = ({ getGridDataRef.current = () => { const gv = gridRef.current?.gridView as | { - getDataSource?: () => { - getRowCount(): number; - getValue(r: number, f: string): unknown; - }; - } + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } | undefined; if (!gv?.getDataSource) return []; const ds = gv.getDataSource(); @@ -1189,29 +1189,37 @@ const DataGrid = ({ style={{ display: "none" }} onChange={handleFileChange} /> -
+
{statusInfo && (
진행상황:{" "} {statusInfo.laeSbmsnSttsCd === null || - statusInfo.laeSbmsnSttsCd === "0" + statusInfo.laeSbmsnSttsCd === "0" ? "미작성" : statusInfo.laeSbmsnSttsCd === "1" - ? "작성중" - : statusInfo.laeSbmsnSttsCd === "2" - ? "제출" - : statusInfo.laeSbmsnSttsCd === "3" - ? "승인" - : statusInfo.laeSbmsnSttsCd === "4" - ? "반려" - : "검증중"} + ? 작성중 + : statusInfo.laeSbmsnSttsCd === "2" + ? 제출 + : statusInfo.laeSbmsnSttsCd === "3" + ? 승인 + : statusInfo.laeSbmsnSttsCd === "4" + ? ( + + 반려 + + + + + + ) + : 검증중} 제출일:{" "} {statusInfo.laeSbmsnYmd ? statusInfo.laeSbmsnYmd.length === 8 && - /^\d{8}$/.test(statusInfo.laeSbmsnYmd) + /^\d{8}$/.test(statusInfo.laeSbmsnYmd) ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` : statusInfo.laeSbmsnYmd : "미제출"} @@ -1228,26 +1236,28 @@ const DataGrid = ({
)} - {isExcelImport && ( - - )} - {isExcelExport && ( - - )} +
+ {isExcelImport && ( + + )} + {isExcelExport && ( + + )} +
boolean; } ).onSorting = enableSort - ? createOnSortingHandler( - columns, - setRowData, - sortDirRef, - originalRowsRef, - ) - : () => false; + ? createOnSortingHandler( + columns, + setRowData, + sortDirRef, + originalRowsRef, + ) + : () => false; const dataProvider = gridRef.current ?.dataProvider as unknown as | { - setFields?(f: unknown[]): void; - setRows?( - r: Record[], - ): void; - getJsonRow?( - r: number, - ): Record; - } + setFields?(f: unknown[]): void; + setRows?( + r: Record[], + ): void; + getJsonRow?( + r: number, + ): Record; + } | undefined; if (dataProvider?.setFields) { dataProvider.setFields( @@ -1436,14 +1446,14 @@ const DataGrid = ({ })} footer={ column.footer && - !Array.isArray(column.footer) + !Array.isArray(column.footer) ? { - text: column.footer.text, - expression: - column.footer.expression, - numberFormat: - column.footer.numberFormat, - } + text: column.footer.text, + expression: + column.footer.expression, + numberFormat: + column.footer.numberFormat, + } : undefined } mergeRule={ diff --git a/apps/st/src/components/module/GridTopActionButton.tsx b/apps/st/src/components/module/GridTopActionButton.tsx index d2d8be50e..ae6b3ee4a 100644 --- a/apps/st/src/components/module/GridTopActionButton.tsx +++ b/apps/st/src/components/module/GridTopActionButton.tsx @@ -4,15 +4,20 @@ import { useState } from "react"; import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components"; import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm"; +import type { SearchFormValues } from "../type/SearchForm.type"; type GridTopActionButtonProps = { text: string; onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void; /** 상위 화면 검색 통계연도 — 시설 조회 다이얼로그에 전달 */ statsYr?: string; + /** 상위 화면 최근 검색조건 */ + initialSearchValues?: Pick | null; + /** 다이얼로그 저장 성공 후 부모 갱신 */ + onSaveSuccess?: () => Promise | void; }; -const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProps) => { +const GridTopActionButton = ({ text, onClick, statsYr, initialSearchValues, onSaveSuccess }: GridTopActionButtonProps) => { const [isOpen, setIsOpen] = useState(false); const [title, setTitle] = useState(text); @@ -41,7 +46,11 @@ const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProp {title} - + diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index b5cdb06a4..b48a6b6c3 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -89,6 +89,7 @@ const SearchForm = (props: SearchFormProps = {}) => { fctName: props.defaultValues?.fctName || "", bsnmCd: props.defaultValues?.bsnmCd || "", isDetail: false, + isSummary: props.defaultValues?.isSummary || "", }); const sggKey: [string, string, string, string] | null = @@ -132,6 +133,7 @@ const SearchForm = (props: SearchFormProps = {}) => { fctName: "", bsnmCd: "", isDetail: false, + isSummary: "", }; setValues(resetValues); props.onReset?.(); @@ -153,7 +155,7 @@ const SearchForm = (props: SearchFormProps = {}) => { const selectedBsnm = value === "ALL" ? sidoList?.data.sidoList.find((v) => v.sido === values.sido) - ?.bsnmCd || "" + ?.bsnmCd || "" : listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ""; setValues((prev) => ({ @@ -250,10 +252,11 @@ const SearchForm = (props: SearchFormProps = {}) => { )} {mergedProps.isDetail && (
-
)} + {mergedProps.isSummary && ( +
+ + +
+ )} {mergedProps.order && (
diff --git a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx index d8647037e..82a1bae17 100644 --- a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx +++ b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useSWR from "swr"; import { ValueType } from "realgrid"; @@ -29,8 +29,13 @@ import type { ColumnInfo } from "../type/DataGrid.type"; import type { SeweragePipeFacilitiesListRequest, SeweragePipeFacilityAdminAreaListRequest, + SeweragePipeFactConnectionSaveRequest, + SeweragePipeFactConnectionSaveItem, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; +import type { SearchFormValues } from "../type/SearchForm.type"; +import { doNameList } from "../util/column/input/DoList"; +import { getUserInfo } from "../../../../../packages/common/components/src/api"; /** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */ const treatmentFacilityColumnInfo: ColumnInfo = { @@ -48,21 +53,35 @@ const treatmentFacilityColumnInfo: ColumnInfo = { editable: false, }, { - name: "fctNm", - fieldName: "fctNm", + name: "fcltNm", + fieldName: "fcltNm", header: { text: "시설명" }, editable: false, }, { - name: "fctCpc", - fieldName: "fctCpc", + name: "fcltCd", + fieldName: "fcltCd", + header: { text: "시설코드" }, + editable: false, + visible: false, + }, + { + name: "fcltSn", + fieldName: "fcltSn", + header: { text: "시설일련번호" }, + editable: false, + visible: false, + }, + { + name: "fcltCpct", + fieldName: "fcltCpct", header: { text: "시설용량(m³/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false, }, ], - columnLayout: ["ctpvNm", "sggNm", "fctNm", "fctCpc"], + columnLayout: ["ctpvNm", "sggNm", "fcltNm", "fcltCpct", "fcltCd"], }; /** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */ @@ -103,12 +122,26 @@ const getSggAllListFetcher = async ( type SidoSggFacilityNameSearchFormProps = { /** 상위(SewagePipeInp 등) 검색 조건의 통계연도 — facilitiesList 등 API 필수값 */ statsYr?: string; + /** 상위(SewagePipeInp) 마지막 검색 조건 */ + initialSearchValues?: Pick | null; + /** 저장 성공 후 부모 탭 데이터 재조회 훅 */ + onSaveSuccess?: () => Promise | void; }; const SidoSggFacilityNameSearchForm = ({ statsYr, + initialSearchValues, + onSaveSuccess, }: SidoSggFacilityNameSearchFormProps) => { const { setMessage: alert } = useAlertStore((state) => state); + const initialSidoValue = + initialSearchValues?.sido && initialSearchValues.sido !== "ALL" + ? initialSearchValues.sido + : "ALL"; + const initialGugunValue = + initialSearchValues?.gugun && initialSearchValues.gugun !== "ALL" + ? initialSearchValues.gugun + : "ALL"; const { data: sidoList } = useSWR>( "sido-list", @@ -134,9 +167,9 @@ const SidoSggFacilityNameSearchForm = ({ }, ); - const [sido, setSido] = useState("ALL"); - const [gugun, setGugun] = useState("ALL"); - const [fctNm, setFctNm] = useState(""); + const [sido, setSido] = useState(initialSidoValue); + const [gugun, setGugun] = useState(initialGugunValue); + const [fcltNm, setFcltNm] = useState(""); const [isLoading, setIsLoading] = useState(false); const [treatmentFacilityRows, setTreatmentFacilityRows] = useState< @@ -145,6 +178,10 @@ const SidoSggFacilityNameSearchForm = ({ const [adminRegionRows, setAdminRegionRows] = useState< Record[] >([]); + const [selectedFacilityRow, setSelectedFacilityRow] = useState< + Record | null + >(null); + const didAutoFetchRef = useRef(false); const sggKey: [string, string] | null = sido && sido !== "ALL" ? ["sgg-all", sido] : null; @@ -198,7 +235,7 @@ const SidoSggFacilityNameSearchForm = ({ alert("시도를 선택해주세요."); return; } - const trimmedFclty = fctNm.trim(); + const trimmedFclty = fcltNm.trim(); const sewerBzmnCd = resolveSewerBzmnCd(); @@ -225,13 +262,15 @@ const SidoSggFacilityNameSearchForm = ({ list.map((item) => ({ ctpvNm: item.ctpvNm, sggNm: item.sggNm, - fctNm: item.bscFctNm, - fctCpc: item.fctCpc, - bscFctCd: item.bscFctCd, + fcltNm: item.fcltNm, + fcltCpct: item.fcltCpct, + fcltCd: item.fcltCd, + fcltSn: item.fcltSn, admdstCd: item.admdstCd, statsYr: item.statsYr, })), ); + setSelectedFacilityRow(null); setAdminRegionRows([]); } catch { alert("처리시설 목록 조회 중 오류가 발생했습니다."); @@ -247,7 +286,7 @@ const SidoSggFacilityNameSearchForm = ({ alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); return; } - const swtrmtFcltCd = String(row.bscFctCd ?? "").trim(); + const swtrmtFcltCd = String(row.fcltCd ?? "").trim(); if (!swtrmtFcltCd) { alert("처리시설 코드가 없습니다."); return; @@ -261,13 +300,14 @@ const SidoSggFacilityNameSearchForm = ({ const params: SeweragePipeFacilityAdminAreaListRequest = { statsYr: statsYr.trim(), swtrmtFcltCd, - sewerBzmnCd: String(row.admdstCd ?? "").trim(), + sewerBzmnCd: initialSearchValues?.bsnmCd || '', ctpvNm, - sggNm, + ...(doNameList.includes(ctpvNm) ? { sggNm } : {}), }; try { setIsLoading(true); + setSelectedFacilityRow(row); const response = await statisticsInputSewPipeApiService.getFacilityAdminAreaList( params, @@ -283,6 +323,8 @@ const SidoSggFacilityNameSearchForm = ({ sggNm: item.sggNm, emdNm: item.admdstNm, admdstCd: item.admdstCd, + isConnection: item.isConnection, + __checked: item.isConnection === "1" ? "1" : "0", })), ); } catch { @@ -293,9 +335,172 @@ const SidoSggFacilityNameSearchForm = ({ }; const handleSaveAdminRegions = () => { - alert("저장 API 연동 예정입니다."); + if (!statsYr?.trim()) { + alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); + return; + } + if (!selectedFacilityRow) { + alert("처리시설을 먼저 선택해주세요."); + return; + } + + const hasChanged = adminRegionRows.some( + (row) => + (row.__checked === "1" && row.isConnection !== "1") || + (row.__checked !== "1" && row.isConnection === "1"), + ); + + const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows + .filter((row) => row.__checked === "1") + .map((row) => ({ + admdstCd: String(row.admdstCd ?? ""), + admdstNm: String(row.emdNm ?? ""), + })) + .filter((row) => row.admdstCd && row.admdstNm); + + const deleteList = adminRegionRows + .filter((row) => row.__checked !== "1" && row.isConnection === "1") + .map((row) => String(row.admdstCd ?? "")) + .filter(Boolean); + + if (!hasChanged) { + alert("변경된 연결 정보가 없습니다."); + return; + } + + const run = async () => { + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const request: SeweragePipeFactConnectionSaveRequest = { + ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""), + sggNm: String(selectedFacilityRow.sggNm ?? ""), + fcltSn: String(selectedFacilityRow.fcltSn ?? ""), + statsYr: statsYr.trim(), + swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""), + swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""), + prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0), + sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "", + userId: userInfo.userUnqId, + saveList, + deleteList, + }; + + const response = + await statisticsInputSewPipeApiService.saveFactConnection( + request, + ); + + if (response.status !== "success") { + alert(response.message ?? "연결관계 저장에 실패했습니다."); + return; + } + + setAdminRegionRows((prev) => + prev.map((row) => ({ + ...row, + isConnection: row.__checked === "1" ? "1" : "0", + })), + ); + await onSaveSuccess?.(); + alert("저장되었습니다."); + } catch { + alert("연결관계 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + void run(); }; + const handleAdminRegionItemChecked = ( + itemIndex: number, + checked: boolean, + ) => { + setAdminRegionRows((prev) => + prev.map((row, idx) => + idx === itemIndex + ? { ...row, __checked: checked ? "1" : "0" } + : row, + ), + ); + }; + + const handleAdminRegionItemAllChecked = (checked: boolean) => { + setAdminRegionRows((prev) => + prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })), + ); + }; + + useEffect(() => { + if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return; + setSido(initialSearchValues.sido); + setGugun( + initialSearchValues.gugun && initialSearchValues.gugun !== "ALL" + ? initialSearchValues.gugun + : "ALL", + ); + }, [initialSearchValues?.sido, initialSearchValues?.gugun]); + + useEffect(() => { + if (didAutoFetchRef.current) return; + if (!statsYr?.trim()) return; + if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return; + + const nextSido = initialSearchValues.sido; + const nextGugun = + initialSearchValues.gugun && initialSearchValues.gugun !== "ALL" + ? initialSearchValues.gugun + : "ALL"; + + didAutoFetchRef.current = true; + setSido(nextSido); + setGugun(nextGugun); + + const params: SeweragePipeFacilitiesListRequest = { + statsYr: statsYr.trim(), + ...(initialSearchValues.bsnmCd?.trim() + ? { sewerBzmnCd: initialSearchValues.bsnmCd.trim() } + : {}), + ctpvNm: nextSido, + ...(nextGugun !== "ALL" ? { sggNm: nextGugun } : {}), + }; + + const run = async () => { + try { + setIsLoading(true); + const response = + await statisticsInputSewPipeApiService.getFacilitiesList(params); + if (response.status !== "success") { + alert(response.message ?? "처리시설 목록 조회에 실패했습니다."); + return; + } + const list = response.data ?? []; + setTreatmentFacilityRows( + list.map((item) => ({ + ctpvNm: item.ctpvNm, + sggNm: item.sggNm, + fcltNm: item.fcltNm, + fcltCpct: item.fcltCpct, + fcltCd: item.fcltCd, + fcltSn: item.fcltSn, + admdstCd: item.admdstCd, + statsYr: item.statsYr, + })), + ); + setSelectedFacilityRow(null); + setAdminRegionRows([]); + } catch { + alert("처리시설 목록 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + void run(); + }, [statsYr, initialSearchValues, alert]); + return (
@@ -346,8 +551,8 @@ const SidoSggFacilityNameSearchForm = ({ setFctNm(e.target.value)} + value={fcltNm} + onChange={(e) => setFcltNm(e.target.value)} placeholder="시설명 입력" />
@@ -396,6 +601,8 @@ const SidoSggFacilityNameSearchForm = ({ rowData={adminRegionRows} setRowData={setAdminRegionRows} showHeaderCheckbox + onItemChecked={handleAdminRegionItemChecked} + onItemAllChecked={handleAdminRegionItemAllChecked} compactFullWidth // compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]" /> diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 91eaaacc3..5ea57e205 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useRef } from "react"; +import { useState, useMemo, useRef, useEffect } from "react"; import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; @@ -7,41 +7,44 @@ import GridTopActionButton from "@/components/module/GridTopActionButton"; import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { useAdminStore } from "@/store/admin-store"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { doList } from "@/components/util/column/input/DoList"; import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; import type { + SeweragePipeInputFactInstallStatusSaveRequest, SeweragePipeInputRegionalFactStatusSaveRequest, SeweragePipeInputStatusInfo, + SeweragePipeInputVerificationResult, SeweragePipeRequestParams, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; -const applyEnterableFromStatus = ( +/** 탭별 status + 입력기간으로 편집 가능 여부 */ +const computeTabEnterable = ( statusInfo: SeweragePipeInputStatusInfo | null | undefined, periodEnterable: boolean, - setIsEnterable: (v: boolean) => void -) => { +): boolean => { if (statusInfo?.laeSbmsnSttsCd) { if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { - setIsEnterable(false); - } else { - setIsEnterable(periodEnterable); + return false; } - } else { - setIsEnterable(periodEnterable); + return periodEnterable; } + return periodEnterable; }; function buildSewagePipeSearchParam( @@ -58,6 +61,10 @@ function buildSewagePipeSearchParam( } /** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */ +type GridRowWithVerification = Record & { + verificationResult?: SeweragePipeInputVerificationResult; +}; + function tabRowDataExists( m: Map[]>, tabKey: string, @@ -74,6 +81,43 @@ function tabRowDataExists( return Array.isArray(rows) && rows.length > 0; } +/** 조회 직후 해당 탭에만 저장하는 상단 툴바 노출 조건 */ +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +/** + * @param searchContext 조회에 사용된 검색값(해당 요청 기준으로 구군·사업자 조건 반영) + * @param treatAsSearched (hasRow || 이 값) 일 때 카드 후보 — load 내부에서는 searchDoneRef 반영용 + */ +function computeTabToolbarSnapshot( + statusInfo: SeweragePipeInputStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + (statusInfo?.laeSbmsnSttsCd === "2" && + (isAdminInfo || (enterable && gugunOk)))); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + // 하수도 통계 관리 > 02. 하수관로 const SewagePipeInp = () => { const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); @@ -81,19 +125,31 @@ const SewagePipeInp = () => { const [activeTab, setActiveTab] = useState(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"); const [isLoading, setIsLoading] = useState(false); const [searchValues, setSearchValues] = useState(null); - const [isEnterable, setIsEnterable] = useState(true); const [lastSearchedYear, setLastSearchedYear] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); - const [statusInfo, setStatusInfo] = useState(null); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); // const [showDailog, setShowDailog] = useState(false); // const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); - const [_statsCd, setStatsCd] = useState(''); + const [statsCd, setStatsCd] = useState(''); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); /** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */ const [hasSearched, setHasSearched] = useState(false); - // const { confirm } = useConfirm(); + /** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */ + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + /** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */ + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState>( + () => new Map(), + ); + const { confirm } = useConfirm(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const globalProgressVisible = useGlobalProgressStore((state) => state.visible); const { isAdminInfo } = useAdminStore(); const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getFactInstallGridDataRef = useRef<(() => Record[]) | null>(null); const searchParamRef = useRef(null); /** 검색 시 checkInputPeriod 결과 — 탭 전환 지연 조회 시 편집 가능 여부와 동일하게 맞춤 */ const periodEnterableRef = useRef(true); @@ -102,23 +158,41 @@ const SewagePipeInp = () => { const lazyTabLoggedRef = useRef>(new Set()); const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"; + const activeTabRef = useRef(firstTabKey); + + useEffect(() => { + activeTabRef.current = activeTab; + }, [activeTab]); + + const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => { + if (loadedTabKey === activeTabRef.current) { + setStatsCd(nextStatsCd); + } + }; const handleReset = () => { setRowDataMap(new Map()); - setStatusInfo(null); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); setLastSearchedYear(null); setLastSearchedValues(null); setHasSearched(false); + setInputPeriodEnterable(true); + setToolbarSnapshotByTab(new Map()); searchParamRef.current = null; searchDoneRef.current = false; lazyTabLoggedRef.current.clear(); periodEnterableRef.current = true; + activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); + setStatsCd(""); }; const columnInfo = useMemo(() => { - const canEdit = isEnterable || isAdminInfo; return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => { + const canEdit = + computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || + isAdminInfo; const applyEditable = (cols: ColumnConfig[] | undefined) => (cols ?? []).map((col: ColumnConfig) => canEdit @@ -138,14 +212,36 @@ const SewagePipeInp = () => { columns: applyEditable(tab.columns), }; }); - }, [isEnterable, isAdminInfo]); + }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SeweragePipeInputStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); + }; /** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */ const loadSewagePipeActiveTabData = async ( tabKey: string, searchParam: SeweragePipeRequestParams, values: SearchFormValues, - checkEnterable: boolean, mergeIntoExistingMap: boolean, updateLastSearchedValues: boolean, ): Promise => { @@ -172,10 +268,16 @@ const SewagePipeInp = () => { ...d, })); applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData))); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + convertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -190,10 +292,16 @@ const SewagePipeInp = () => { ...d, })); applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData))); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + convertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -208,10 +316,16 @@ const SewagePipeInp = () => { ...d, })); applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData))); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + convertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -226,10 +340,16 @@ const SewagePipeInp = () => { ...d, })); applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData))); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + convertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -244,10 +364,16 @@ const SewagePipeInp = () => { ...d, })); applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData))); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + convertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -268,10 +394,16 @@ const SewagePipeInp = () => { m.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData)); m.set('adminAreaConnectionInp', toGridRows(adminConvertedData)); }); - setStatusInfo(response.data.statusInfo); - setStatsCd(commonInfo.statsCd); - applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); + commitToolbarSnapshotForLoadedTab( + tabKey, + response.data.statusInfo, + facilityConvertedData.length > 0 || adminConvertedData.length > 0, + values, + ); return true; } alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); @@ -281,7 +413,6 @@ const SewagePipeInp = () => { setRowDataMap(new Map()); } if (updateLastSearchedValues) setLastSearchedValues(values); - applyEnterableFromStatus(null, checkEnterable, setIsEnterable); return true; }; @@ -299,8 +430,21 @@ const SewagePipeInp = () => { return; } - const checkEnterable = periodResponse.data; - periodEnterableRef.current = checkEnterable; + const periodOk = isInputPeriodCurrent(periodResponse.data); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + /** 연도가 바뀐 검색만 전 탭 캐시 초기화 + 첫 탭으로 이동. 연도 외 조건만 바뀌면 다른 탭은 이전 조회 데이터 유지 */ + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setStatsCd(""); + setToolbarSnapshotByTab(new Map()); + } lazyTabLoggedRef.current.clear(); @@ -312,7 +456,15 @@ const SewagePipeInp = () => { setHasSearched(true); setLastSearchedYear(values.year ?? null); - await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, true, true); + const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTab; + + if (yearChangedFromLastSearch) { + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } else { + activeTabRef.current = activeTab; + } + await loadSewagePipeActiveTabData(tabKeyToLoad, searchParam, values, true, true); } catch { alert('데이터 조회 중 오류가 발생했습니다.'); } finally { @@ -321,7 +473,9 @@ const SewagePipeInp = () => { }; const handleActiveTabChange = async (tabKey: string) => { + activeTabRef.current = tabKey; setActiveTab(tabKey); + setStatsCd(tabStatsCdMap.get(tabKey) ?? ""); const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey); @@ -339,14 +493,7 @@ const SewagePipeInp = () => { try { setIsLoading(true); - const ok = await loadSewagePipeActiveTabData( - tabKey, - searchParam, - values, - periodEnterableRef.current, - true, - false, - ); + const ok = await loadSewagePipeActiveTabData(tabKey, searchParam, values, true, false); if (ok) lazyTabLoggedRef.current.add(tabKey); } catch { alert('데이터 조회 중 오류가 발생했습니다.'); @@ -355,11 +502,6 @@ const SewagePipeInp = () => { } }; - const SubmitExceptionFunc = () => { - console.log('submit exception'); - // 예외 제출 로직 - }; - const getLastYearDataFunc = async () => { const values = lastSearchedValues ?? searchValues; if (!values?.sido || values.sido === 'ALL') { @@ -373,7 +515,7 @@ const SewagePipeInp = () => { try { setIsLoading(true); const param = buildSewagePipeSearchParam(values, { mode: 'PRE' }); - await loadSewagePipeActiveTabData(activeTab, param, values, periodEnterableRef.current, true, false); + await loadSewagePipeActiveTabData(activeTab, param, values, true, false); } catch { alert('데이터 조회 중 오류가 발생했습니다.'); } finally { @@ -386,115 +528,237 @@ const SewagePipeInp = () => { // 제출 로직 }; - const saveFunc = async () => { - if (activeTab !== "ST00020001") { + const refreshConnectionStatusAfterPopupSave = async () => { + const searchParam = searchParamRef.current; + const values = lastSearchedValues ?? searchValues; + if (!searchParam || !values) return; + await loadSewagePipeActiveTabData("ST00020006", searchParam, values, true, false); + }; + + const openVerificationHistoryDialog = () => { + const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? statsCd; + if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !activeTabStatsCd) { + alert("먼저 조회를 진행해주세요."); return; } + setVerificationHistoryOpen(true); + }; + + const saveFunc = async () => { + if (!(await confirm("저장 하시겠습니까?"))) return; + + const values = lastSearchedValues ?? searchValues; + if (!values?.year) { + alert("검색 후 저장해 주세요."); + return; + } + try { - const currentData = getGridDataRef.current?.(); - if (!currentData || currentData.length === 0) { - alert("저장할 데이터가 없습니다."); - return; - } - const values = lastSearchedValues ?? searchValues; - if (!values?.year) { - alert("검색 후 저장해 주세요."); - return; - } + showGlobalProgress({ label: "저장 진행 중...", progress: 0 }); - const columnFormatMap = new Map(); - RegionalFactStatusInpColumn.columns.forEach((col) => { - if (col.fieldName && col.dataFormat) { - columnFormatMap.set(col.fieldName, col.dataFormat); + switch (activeTab) { + case "ST00020001": { + const currentData = getGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const columnFormatMap = new Map(); + RegionalFactStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + + const userInfo = await getUserInfo(); + + const saveList = currentData.map((d) => ({ + admdstCd: String(d.admdstCd ?? ""), + sggNm: String(d.sggNm ?? ""), + admdstNm: String(d.admdstNm ?? ""), + goalYr: String(d.goalYr ?? ""), + wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), + wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), + wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), + swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"), + cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"), + cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"), + cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"), + cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"), + cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"), + cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"), + cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"), + cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"), + drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"), + drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"), + drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"), + drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"), + drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"), + drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"), + exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"), + exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"), + exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"), + exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"), + exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"), + exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"), + exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"), + exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"), + cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"), + sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"), + sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"), + drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"), + exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"), + stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"), + exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), + sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), + sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), + rmrk: (d.rmrk as string | null) ?? null, + })); + + const param: SeweragePipeInputRegionalFactStatusSaveRequest = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? "superAdmin" : "", + saveList, + }; + + const response = + await statisticsInputSewPipeApiService.saveRegionalFactStatus(param); + if (response.status === "success") { + setGlobalProgress(50); + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + setGlobalProgress(100); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + break; } - }); + case "ST00020002": { + const currentData = getFactInstallGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } - const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { - if (value === null || value === undefined || value === "") return 0; - const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; - if (isNaN(num)) return 0; - const format = columnFormatMap.get(fieldName); - if (!format) return num; - const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; - return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); - }; + const columnFormatMap = new Map(); + FactInstallStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); - const userInfo = await getUserInfo(); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; - const saveList = currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ""), - sggNm: String(d.sggNm ?? ""), - admdstNm: String(d.admdstNm ?? ""), - goalYr: String(d.goalYr ?? ""), - wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), - wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), - wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), - swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"), - cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"), - cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"), - cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"), - cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"), - cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"), - cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"), - cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"), - cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"), - drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"), - drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"), - drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"), - drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"), - drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"), - drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"), - exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"), - exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"), - exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"), - exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"), - exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"), - exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"), - exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"), - exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"), - cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"), - sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"), - sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"), - drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"), - exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"), - stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"), - exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), - sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), - sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), - rmrk: (d.rmrk as string | null) ?? null, - })); + const userInfo = await getUserInfo(); - const param: SeweragePipeInputRegionalFactStatusSaveRequest = { - statsYr: values.year ?? "", - ctpvNm: values.sido ?? "", - sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), - sewerBzmnCd: values.bsnmCd ?? "", - userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", - saveList, - }; + const saveList = currentData.map((d) => ({ + admdstCd: String(d.admdstCd ?? ""), + sggNm: String(d.sggNm ?? ""), + admdstNm: String(d.admdstNm ?? ""), + fcltSn: String(d.fcltSn ?? ""), + swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), + swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), + prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"), + prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), + cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), + sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"), + sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"), + mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"), + cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"), + sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"), + sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"), + sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"), + wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), + wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"), + icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"), + atlfcsPrlg: formatNumber(d.atlfcsPrlg, "atlfcsPrlg"), + cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"), + sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"), + sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"), + wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"), + cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), + exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), + drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), + rmrk: String(d.rmrk ?? ""), + })); - console.log(param); + const param: SeweragePipeInputFactInstallStatusSaveRequest = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? "superAdmin" : "", + saveList, + }; + + const response = + await statisticsInputSewPipeApiService.saveFactInstallStatus(param); + if (response.status === "success") { + setGlobalProgress(50); + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + setGlobalProgress(100); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + break; + } + default: + alert("현재 탭 저장 API 연동 전입니다."); + break; + } } catch { - alert("저장 준비 중 오류가 발생했습니다."); + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); } }; - const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '') - ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') - : true; + const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues); - const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList; - const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList; + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + tabStatusInfoMap.get(activeTab) ?? null, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); - /** rowDataMap — 행이 하나라도 있을 때만 툴바·엑셀 의미 있음 */ - const hasRowData = Array.from(rowDataMap.values()).some((rows) => rows.length > 0); - const excelToolbarEnabled = hasRowData && isEnterable && gugunOkOrNotDoList; - - const showActionToolbarCard = - (hasRowData || hasSearched) && - (showSaveInToolbar || - (statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList)))); + const { + showSaveInToolbar, + showEnterableToolbarButtons, + showActionToolbarCard, + } = activeTabToolbar; return ( <> @@ -502,9 +766,11 @@ const SewagePipeInp = () => { {showActionToolbarCard && (
+ {showEnterableToolbarButtons && ( <> - void getLastYearDataFunc()} /> @@ -513,7 +779,7 @@ const SewagePipeInp = () => {
)} - {isLoading &&
+ {isLoading && !globalProgressVisible &&
@@ -534,6 +800,14 @@ const SewagePipeInp = () => { {columnInfo.map((info) => { const tabKey = info.tabKey; + const tabEnterableForGrid = computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ); + const excelToolbarEnabledForTab = + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOkOrNotDoList; if (info.isSplitGrid && info.left && info.right) { const leftKey = info.left.tabKey || `${tabKey}_left`; @@ -544,7 +818,7 @@ const SewagePipeInp = () => { const showSplitTopButton = Boolean(splitTopButton?.text) && (splitTopButton?.visible ?? true) && - excelToolbarEnabled; + excelToolbarEnabledForTab; const leftColumnInfo: ColumnInfo = { columns: info.left.columns ?? [], columnLayout: info.left.columnLayout ?? [], @@ -563,6 +837,16 @@ const SewagePipeInp = () => { text={splitTopButton?.text ?? ""} onClick={splitTopButton?.onClick} statsYr={lastSearchedYear ?? undefined} + initialSearchValues={ + lastSearchedValues + ? { + sido: lastSearchedValues.sido, + gugun: lastSearchedValues.gugun, + bsnmCd: lastSearchedValues.bsnmCd, + } + : null + } + onSaveSuccess={refreshConnectionStatusAfterPopupSave} /> )}
@@ -618,6 +902,7 @@ const SewagePipeInp = () => { return ( { return next; }); }} - isExcelImport={excelToolbarEnabled} + isExcelImport={excelToolbarEnabledForTab} isExcelExport showHeaderCheckbox={false} fixedColumns={info.fixedColumns} enableSort={false} - getGridDataRef={tabKey === "ST00020001" ? getGridDataRef : undefined} + statusInfo={tabStatusInfoMap.get(tabKey) ?? null} + getGridDataRef={ + tabKey === "ST00020001" + ? getGridDataRef + : tabKey === "ST00020002" + ? getFactInstallGridDataRef + : undefined + } + cellStyleCallback={({ row, fieldName }) => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }} + cellTooltipCallback={({ row, fieldName }) => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" ? "IMR검증 - " : "일반검증 - "; + const comp = e.compVlCn ?? ""; + const now = e.nowVlCn ?? ""; + return `${prefix}${e.rsnExpln}(검증 기준값: ${comp} / 현재 입력값: ${now})`; + }) + .filter(Boolean) + .join("\n"); + } + }} /> ); })}
+ ); }; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index c37700dae..c31a4e060 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -24,13 +24,16 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap import { useConfirm } from "@frontend-opensource/use-react-hooks"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import type { - SewerageSupplyRateItem, - SewerageSupplyRateRequestParams, - SewerageSupplyRateSaveItem, - SewerageSupplyRateVerificationResult, - SewerageSupplyRateSaveRequest, - SewerageSupplyRateStatusInfo, +import { + type CheckInputPeriodItem, + type SewerageSupplyRateItem, + type SewerageSupplyRateRequestParams, + type SewerageSupplyRateSaveItem, + type SewerageSupplyRateVerificationResult, + type SewerageSupplyRateSaveRequest, + type SewerageSupplyRateStatusInfo, + getVerificationPeriodActionBlockedMessage, + isInputPeriodCurrent, } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import type { cancelSubmitRequest, @@ -100,6 +103,8 @@ const SeweragePenetrationRateInp = () => { const [lastSearchedYear, setLastSearchedYear] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); const [statusInfo, setStatusInfo] = useState(null); + /** 마지막 조회 시 check-input-period 응답 (검증기간 여부 판단) */ + const [periodCheckList, setPeriodCheckList] = useState([]); const [showDailog, setShowDailog] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); @@ -147,6 +152,7 @@ const SeweragePenetrationRateInp = () => { const handleReset = () => { setRowData([]); setStatusInfo(null); + setPeriodCheckList([]); setLastSearchedYear(null); setLastSearchedValues(null); setFooterFromApi(null); @@ -218,8 +224,9 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || ''); if (response.status === 'success') { - const checkEnterable = response.data; - // setIsEnterable(response.data); + const periods = Array.isArray(response.data) ? response.data : []; + setPeriodCheckList(periods); + const checkEnterable = isInputPeriodCurrent(periods); setLastSearchedYear(values.year || null); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable); setLastSearchedValues(values); @@ -232,6 +239,11 @@ const SeweragePenetrationRateInp = () => { }; const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } const userInfo = await getUserInfo(); const params: SubmitExceptionRequest = { @@ -309,6 +321,11 @@ const SeweragePenetrationRateInp = () => { }; const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } try { showGlobalProgress({ label: "제출 진행 중...", progress: 0 }); const userInfo = await getUserInfo(); @@ -388,7 +405,7 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { setGlobalProgress(50); - + if (response.data.finalStatus === '5') { const imrParam = { user_id: userInfo.userUnqId, @@ -442,6 +459,11 @@ const SeweragePenetrationRateInp = () => { const saveFunc = async () => { try { if (await confirm('저장 하시겠습니까?')) { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } showGlobalProgress({ label: "저장 진행 중...", progress: 0 }); const userInfo = await getUserInfo(); @@ -520,9 +542,9 @@ const SeweragePenetrationRateInp = () => { sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계 }; - // if (isAdminInfo) { - // requestData.superAdmin = 'superAdmin'; - // } + if (isAdminInfo) { + requestData.superAdmin = 'superAdmin'; + } const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); @@ -547,6 +569,11 @@ const SeweragePenetrationRateInp = () => { const cnacelSubmitFunc = async () => { try { if (await confirm('제출된 내용을 취소하시겠습니까?')) { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } const userInfo = await getUserInfo(); @@ -600,11 +627,11 @@ const SeweragePenetrationRateInp = () => { {showActionToolbarCard && (
+ {showEnterableToolbarButtons && ( <> - getLastYearDataFunc(searchValues ?? {})} /> submitFunc(false)} /> diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index e2abaf60d..fdb9a4e1a 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -12,7 +12,6 @@ import type { ApiResponse, SearchCondition, Sgg, - StatisticsLog, StatisticsLogRequest, } from "@/entities/statistics/model/statistics-interface"; import { diff --git a/apps/st/src/components/statistics/total/CellColorInfo.tsx b/apps/st/src/components/statistics/total/CellColorInfo.tsx new file mode 100644 index 000000000..a44ee1879 --- /dev/null +++ b/apps/st/src/components/statistics/total/CellColorInfo.tsx @@ -0,0 +1,22 @@ +const CellColorInfo = () => { + return ( +
+
+
+
+ 요약표 +
+
+
+ 합계 +
+
+
+ 소계 +
+
+
+ ); +}; + +export default CellColorInfo; \ No newline at end of file diff --git a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx index 12e71151d..a061146c5 100644 --- a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx @@ -1,10 +1,202 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + import SearchForm from "@/components/module/SearchForm"; +import DataGrid from "@/components/module/DataGrid"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { SewagePipeMaintainColumnListTot } from "@/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface"; + +const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001"; + +function buildSewagePipeMaintainTotSearchParam(values: SearchFormValues): SewagePipeMaintainTotalRequest { + return { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + }; +} const SewagePipeMaintainTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = SewagePipeMaintainColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadSewagePipeMaintainTotTabData = async ( + tabKey: string, + values: SearchFormValues, + mergeIntoExistingMap: boolean, + ): Promise => { + const param = buildSewagePipeMaintainTotSearchParam(values); + + const applyRows = (rows: Record[]) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, rows); + return next; + }); + } else { + const next = new Map[]>(); + next.set(tabKey, rows); + setRowDataMap(next); + } + }; + + if (tabKey === "ST00030001") { + const response = await statisticsTotalStatusApi.getSewagePipeMaintainRepairTotal(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + if (tabKey === "ST00030002") { + const response = await statisticsTotalStatusApi.getSewagePipeMaintainDredgingTotal(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + return false; + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + + let ok = false; + if (activeTab === "ST00030001" || activeTab === "ST00030002") { + ok = await loadSewagePipeMaintainTotTabData(activeTab, values, true); + } + if (ok) { + lastSearchedValuesRef.current = values; + } else { + lastSearchedValuesRef.current = null; + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const values = lastSearchedValuesRef.current; + if (!values) return; + + const existing = rowDataMap.get(tabKey); + const hasData = Array.isArray(existing) && existing.length > 0; + if (hasData) return; + + if (tabKey === "ST00030001" || tabKey === "ST00030002") { + try { + setIsLoading(true); + await loadSewagePipeMaintainTotTabData(tabKey, values, true); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + }; + return ( - <> - - +
+ + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={({ row }) => { + const rowType = String(row.rowType ?? ""); + if (rowType === "DETAIL") return undefined; + if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum"; + return "rg-cell-rowtype-other"; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewagePipeTot.tsx b/apps/st/src/components/statistics/total/SewagePipeTot.tsx index cea602a35..0ee0aac2b 100644 --- a/apps/st/src/components/statistics/total/SewagePipeTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeTot.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useRef, useState } from "react"; import { useAlertStore } from "@/shared/lib"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; @@ -6,71 +6,165 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from " import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; -import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface"; const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001"; +function buildSewagePipeTotSearchParam(values: SearchFormValues): SewagePipeTotalRequest { + return { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + }; +} + const SewagePipeTot = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(firstTabKey); + /** 조회 성공 후 탭 전환 시 동일 조건으로 지연 조회 */ + const lastSearchedValuesRef = useRef(null); - const columnInfo = useMemo( - () => - SewagePipeColumnInfoListTot.map((tab: ColumnInfoList) => ({ - ...tab, - columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ - ...col, - editable: false as const, - styleName: col.styleName ?? ("rg-cell-readonly" as const), - })), - })), - [], - ); + const columnInfo = SewagePipeColumnInfoListTot; const handleReset = () => { setRowDataMap(new Map()); setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + /** SewagePipeInp의 `loadSewagePipeActiveTabData`와 동일 역할: 탭키별 API 호출 후 `rowDataMap` 반영 */ + const loadSewagePipeTotTabData = async ( + tabKey: string, + values: SearchFormValues, + mergeIntoExistingMap: boolean, + ): Promise => { + const param = buildSewagePipeTotSearchParam(values); + + const applyRows = (rows: Record[]) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, rows); + return next; + }); + } else { + const next = new Map[]>(); + next.set(tabKey, rows); + setRowDataMap(next); + } + }; + + if (tabKey === "ST00020001") { + const response = await statisticsTotalStatusApi.getSewagePipeRegionalStatusList(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + if (tabKey === "ST00020002") { + const response = await statisticsTotalStatusApi.getSewagePipeFactInstallStatus(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + if (tabKey === "ST00020003") { + const response = await statisticsTotalStatusApi.getSewagePipePipeTypeFacilityStatus(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + if (tabKey === "ST00020004") { + const response = await statisticsTotalStatusApi.getSewagePipePipeDiamStatus(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + + return false; }; const handleSearch = async (values: SearchFormValues) => { - setActiveTab(firstTabKey); - setRowDataMap(new Map()); - try { setIsLoading(true); - const param = { - ctpvNm: values.sido === "ALL" ? "" : values.sido, - sggNm: values.gugun === "ALL" ? "" : values.gugun, - sewerBzmnCd: values.bsnmCd, - statsYr: values.year, - rowDataYn: values.isDetail, - }; - - const response = await statisticsTotalStatusApi.getSewagePipeRegionalStatusList(param); - - if (response.status === "success" && response.data) { - const next = new Map[]>(); - next.set("ST00020001", toGridRows(response.data as object[])); - setRowDataMap(next); + // SewagePipeInp과 동일: 검색 시 활성 탭 데이터만 조회·맵 병합 + let ok = false; + if ( + activeTab === "ST00020001" || + activeTab === "ST00020002" || + activeTab === "ST00020003" || + activeTab === "ST00020004" + ) { + ok = await loadSewagePipeTotTabData(activeTab, values, true); + } + if (ok) { + lastSearchedValuesRef.current = values; } else { - alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; } } catch { alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; } finally { setIsLoading(false); } }; + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const values = lastSearchedValuesRef.current; + if (!values) return; + + const existing = rowDataMap.get(tabKey); + const hasData = Array.isArray(existing) && existing.length > 0; + if (hasData) return; + + if ( + tabKey === "ST00020001" || + tabKey === "ST00020002" || + tabKey === "ST00020003" || + tabKey === "ST00020004" + ) { + try { + setIsLoading(true); + await loadSewagePipeTotTabData(tabKey, values, true); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + console.log("[SewagePipeTot] tabKey (API 미연결):", tabKey); + }; + return (
- + {isLoading && (
@@ -81,7 +175,7 @@ const SewagePipeTot = () => {
void handleActiveTabChange(v)} className="w-full" variant="default" > diff --git a/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx b/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx index 2ff60a726..7c5d1ab14 100644 --- a/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx +++ b/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx @@ -8,6 +8,7 @@ import { useAlertStore } from "@/shared/lib"; import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; +import CellColorInfo from "./CellColorInfo"; import { LoadingWithAnimation } from "@common/components"; const SeweragePenetrationRateTot = () => { @@ -19,12 +20,17 @@ const SeweragePenetrationRateTot = () => { try { setIsLoading(true); + const isSummaryEnabled = values.isSummary !== 'ALL'; + const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined; + const param = { ctpvNm: values.sido === 'ALL' ? '' : values.sido, sggNm: values.gugun === 'ALL' ? '' : values.gugun, sewerBzmnCd: values.bsnmCd, statsYr: values.year, - rowDataYn: values.isDetail + rowDataYn: values.isDetail, + summaryYn: isSummaryEnabled, + ...(isSummaryEnabled ? { summaryYearCnt } : {}), }; const response = await statisticsTotalStatusApi.getSewerageSupplyRateList(param); @@ -35,7 +41,8 @@ const SeweragePenetrationRateTot = () => { } else { alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); } - } catch { + } catch (error) { + console.log(error) alert('데이터 조회 중 오류가 발생했습니다.'); } finally { setIsLoading(false); @@ -44,12 +51,13 @@ const SeweragePenetrationRateTot = () => { return ( <> - + {isLoading &&
} + { const rowType = String(row.rowType ?? ''); if (rowType === 'DETAIL') return undefined; if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; return 'rg-cell-rowtype-other'; }} /> diff --git a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx index 275dd7a25..452419dc2 100644 --- a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx +++ b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx @@ -1,14 +1,14 @@ -import { useEffect, useMemo, useState } from "react"; +import { useRef, useState } from "react"; import { useAlertStore } from "@/shared/lib"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { toGridRows } from "@/components/util/grid/gridFunction"; import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import { LoadingWithAnimation } from "@common/components"; +import DataGrid from "@/components/module/DataGrid"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; -import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn"; import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn"; @@ -17,81 +17,111 @@ const SewerageServiceLevelInpTabList = [ ComplaintInpColumn, ResidentFriendlyFactTotColumn, ]; +const SUPPORTED_TAB_KEYS = [ComplaintInpColumn.tabKey, ResidentFriendlyFactTotColumn.tabKey] as const; const SewerageServiceLevelTot = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey); - const [lastSearchValues, setLastSearchValues] = useState(null); + const lastSearchValuesRef = useRef(null); - const columnInfo = useMemo( - () => - SewerageServiceLevelInpTabList.map((tab: ColumnInfoList) => ({ - ...tab, - columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ - ...col, - editable: false as const, - styleName: col.styleName ?? ("rg-cell-readonly" as const), - })), - })), - [], - ); + const columnInfo = SewerageServiceLevelInpTabList; const handleReset = () => { setRowDataMap(new Map()); setActiveTab(ComplaintInpColumn.tabKey); - setLastSearchValues(null); + lastSearchValuesRef.current = null; }; - const fetchByTab = async (tabKey: string, values: SearchFormValues) => { - try { - setIsLoading(true); + const loadSewerageServiceLevelTotTabData = async ( + tabKey: string, + values: SearchFormValues, + mergeIntoExistingMap: boolean, + ) => { + const param = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + }; - const param = { - ctpvNm: values.sido === "ALL" ? "" : values.sido, - sggNm: values.gugun === "ALL" ? "" : values.gugun, - sewerBzmnCd: values.bsnmCd, - statsYr: values.year, - rowDataYn: values.isDetail, - }; + const fetcherMap = { + [ComplaintInpColumn.tabKey]: statisticsTotalStatusApi.getSewerageServiceLevelComplaint.bind( + statisticsTotalStatusApi, + ), + [ResidentFriendlyFactTotColumn.tabKey]: + statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact.bind( + statisticsTotalStatusApi, + ), + }; + const fetcher = fetcherMap[tabKey]; + if (!fetcher) { + return false; + } + const response = await fetcher(param); - let response; - if (tabKey === ResidentFriendlyFactTotColumn.tabKey) { - response = - await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param); - } else { - response = - await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param); - } - - if (response.status === "success" && response.data) { + if (response.status === "success" && response.data) { + const rows = toGridRows(response.data as object[]); + if (mergeIntoExistingMap) { setRowDataMap((prev) => { const next = new Map(prev); - next.set(tabKey, toGridRows(response.data as object[])); + next.set(tabKey, rows); return next; }); } else { - alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + const next = new Map[]>(); + next.set(tabKey, rows); + setRowDataMap(next); + } + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + let ok = false; + if (SUPPORTED_TAB_KEYS.includes(activeTab)) { + ok = await loadSewerageServiceLevelTotTabData(activeTab, values, true); + } + if (ok) { + lastSearchValuesRef.current = values; + } else { + lastSearchValuesRef.current = null; } } catch { alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchValuesRef.current = null; } finally { setIsLoading(false); } }; - const handleSearch = async (values: SearchFormValues) => { - setLastSearchValues(values); - setRowDataMap(new Map()); - await fetchByTab(activeTab, values); - }; + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); - useEffect(() => { - if (lastSearchValues == null) return; - if (rowDataMap.has(activeTab)) return; - void fetchByTab(activeTab, lastSearchValues); - }, [activeTab]); + const values = lastSearchValuesRef.current; + if (!values) return; + + const existing = rowDataMap.get(tabKey); + const hasData = Array.isArray(existing) && existing.length > 0; + if (hasData) return; + + if (SUPPORTED_TAB_KEYS.includes(tabKey)) { + try { + setIsLoading(true); + await loadSewerageServiceLevelTotTabData(tabKey, values, true); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + }; return (
@@ -103,20 +133,60 @@ const SewerageServiceLevelTot = () => {
)} - { - const rowType = String(row.rowType ?? ""); - if (rowType === "DETAIL") return undefined; - if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum"; - return "rg-cell-rowtype-other"; - }} - /> +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={({ row }) => { + const rowType = String(row.rowType ?? ""); + if (rowType === "DETAIL") return undefined; + if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum"; + return "rg-cell-rowtype-other"; + }} + /> + + ); + })} + +
); }; diff --git a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx index 0f9da9e01..c27387e85 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx @@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from 'react'; import { NoData } from '@/shared/ui/no-data'; import { Button, - Input, Table, TableBody, TableCell, diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 0ee95e977..706a7bc1d 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -3,9 +3,15 @@ import React from "react"; import { ValueType } from "realgrid"; import type { RealGridReact } from "realgrid-react"; -import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; - // RealGrid 타입 정의 + +/** DataGrid 상단 진행상황/제출일/담당자 표시에 사용 (보급률·하수관로 등 공통) */ +export type DataGridStatusBannerInfo = { + laeSbmsnSttsCd: string | null; + laeSbmsnYmd: string | null; + laePicNm: string | null; + laePicTelNo: string | null; +}; /** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */ export type RealGridColumnInfo = { name: string; @@ -19,30 +25,30 @@ export type RealGridColumnInfo = { mergeRule?: { criteria: "value" | string }; sortable?: boolean; footer?: - | { - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - } - | Array<{ - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - }>; + | { + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + } + | Array<{ + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + }>; }; /** setColumns 메서드가 있는 GridView */ @@ -111,21 +117,21 @@ export type DataFieldValueCallback = ( export type ComputedFieldConfig = | { fields: string[]; type: "sum" | "avg" } | { - numeratorFields: string[]; - denominatorFields: string[]; - type: "pblswrgPopltnSuprtRate"; - } + numeratorFields: string[]; + denominatorFields: string[]; + type: "pblswrgPopltnSuprtRate"; + } | { - numeratorFields: string[]; - denominatorFields: string[]; - type: "adtrtPopltnSuprtRate"; - } + numeratorFields: string[]; + denominatorFields: string[]; + type: "adtrtPopltnSuprtRate"; + } | { - numeratorFields: string[]; - denominatorFields: string[]; - thirdFields: string[]; - type: "swrgInstlrtRate"; - }; + numeratorFields: string[]; + denominatorFields: string[]; + thirdFields: string[]; + type: "swrgInstlrtRate"; + }; export type ColumnConfig = { name: string; @@ -148,31 +154,31 @@ export type ColumnConfig = { valueType?: ValueType; dataFormat?: string; footer?: - | { - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - multiRow?: boolean; // 여러 행 footer 사용 여부 - } - | Array<{ - text?: string; - expression?: "sum" | "avg" | "count" | "max" | "min"; - numberFormat?: string; - valueCallback?: ( - grid: GridView, - column: DataColumn, - footerIndex: number, - columnFooter: ColumnFooter, - value: number | string | undefined, - ) => number | string; - }>; // RealGrid에서 footer를 배열로 설정할 수 있음 + | { + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + multiRow?: boolean; // 여러 행 footer 사용 여부 + } + | Array<{ + text?: string; + expression?: "sum" | "avg" | "count" | "max" | "min"; + numberFormat?: string; + valueCallback?: ( + grid: GridView, + column: DataColumn, + footerIndex: number, + columnFooter: ColumnFooter, + value: number | string | undefined, + ) => number | string; + }>; // RealGrid에서 footer를 배열로 설정할 수 있음 }; export type FooterUserSpan = { @@ -184,16 +190,16 @@ export type FooterUserSpan = { export type ColumnLayoutItem = | string | { - name: string; - hideChildHeaders?: boolean; - header: { text: string }; - items: (string | ColumnLayoutItem)[]; - footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 - } + name: string; + hideChildHeaders?: boolean; + header: { text: string }; + items: (string | ColumnLayoutItem)[]; + footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 + } | { - column: string; // 컬럼 이름 (footerUserSpans 사용 시) - footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 - }; + column: string; // 컬럼 이름 (footerUserSpans 사용 시) + footerUserSpans?: FooterUserSpan[]; // footer 병합 설정 + }; export type ColumnInfo = { columns: ColumnConfig[]; @@ -285,7 +291,7 @@ export type DataGridProps = { /** CheckBar 헤더 전체 선택/해제 */ onItemAllChecked?: (checked: boolean) => void; onScrollToBottom?: () => void; - statusInfo?: SewerageSupplyRateStatusInfo | null; + statusInfo?: DataGridStatusBannerInfo | null; /** 강조 및 스크롤할 행의 ID */ highlightedRowId?: string | number | null; /** ID로 사용할 필드명 (기본값: 'id') */ diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index 6af84e181..79ca5d118 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -6,6 +6,7 @@ export type SearchFormValues = { fctName?: string; bsnmCd?: string; isDetail?: boolean; + isSummary?: string; }; export type SearchFormProps = { @@ -17,6 +18,7 @@ export type SearchFormProps = { fctName?: boolean; userMenual?: boolean; isDetail?: boolean; + isSummary?: boolean; onSubmit?: (values: SearchFormValues) => void; onReset?: () => void; onValuesChange?: (values: SearchFormValues) => void; @@ -33,4 +35,5 @@ export const defaultSearchFormProps: Required< fctName: false, userMenual: false, isDetail: false, + isSummary: false, }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/DoList.ts b/apps/st/src/components/util/column/input/DoList.ts index 2b58bd57b..50085a92f 100644 --- a/apps/st/src/components/util/column/input/DoList.ts +++ b/apps/st/src/components/util/column/input/DoList.ts @@ -20,4 +20,15 @@ export const needBsnmFrgnrPoplSmSidoList = [ "4700000000", "4800000000", "5000000000", +]; + +export const doNameList = [ + "경기도", + "강원특별자치도", + "충청북도", + "충청남도", + "전북특별자치도", + "전라남도", + "경상북도", + "경상남도", ]; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts index 8d99e6c9e..c28f12bc7 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts @@ -49,18 +49,18 @@ export const AdminAreaConnectionInpColumn = { export const TreatPlantConnectionInpColumn = { columns: [ // 하수처리장 정보 - { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false }, - { name: 'fctCpc', fieldName: 'fctCpc', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false }, - { name: 'bscFctCd', fieldName: 'bscFctCd', header: { text: '하수처리장코드' }, editable: false }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '하수처리장명' }, editable: false }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '하수처리장코드' }, editable: false }, // 연결여부 { name: 'isConnection', fieldName: 'isConnection', header: { text: '연결여부' }, editable: false }, // 연결지역 { name: 'connectionAreaList', fieldName: 'connectionAreaList', header: { text: '연결지역' }, editable: false }, ], columnLayout: [ - 'bscFctNm', - 'fctCpc', - 'bscFctCd', + 'fcltNm', + 'fcltCpct', + 'fcltSn', 'isConnection', 'connectionAreaList', ], diff --git a/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts index 549baf09a..9e6b855cf 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts @@ -58,7 +58,7 @@ export const FactInstallStatusInpColumn = { // 식별·키 필드 (API SeweragePipeInputFactInstallStatusItem, 그리드 비표시 — fixedColumns와 겹치지 않도록 맨 뒤) { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'prcsFcltSn', fieldName: 'prcsFcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT }, { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, ], columnLayout: [ @@ -146,7 +146,7 @@ export const FactInstallStatusInpColumn = { }, 'rmrk', 'admdstCd', - 'prcsFcltSn', + 'fcltSn', 'swtrmtFcltCd', ], rowStartNum: 4, diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts index fa55d7f55..9c78c5b0e 100644 --- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts +++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts @@ -1,5 +1,3 @@ -import dayjs from "dayjs"; -import { headers } from "next/headers"; import { ValueType } from "realgrid"; /** @@ -7,9 +5,9 @@ import { ValueType } from "realgrid"; * 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외. * 표시 형식은 SeweragePentrationRateTotColumn과 같이 dataFormat + editable: false. */ -const currentYear = dayjs().year() - 1; -const startYear = 1996; -const totalCount = currentYear - startYear + 1 + 1; + +/** ST00020003 관종별: 입력 `SeweragePipeInputPipeTypeStatusItem`·합계 `SewagePipePipeTypeFacilityStatusItem`과 동일 (yy1=1996년 이전 … yy30) */ +const YY_PIPE_TYPE_FACILITY_MAX = 30; const rawSewagePipeTotList = [ { @@ -642,11 +640,11 @@ const rawSewagePipeTotList = [ dataFormat: "#,##0.0", editable: false, }, - ...Array.from({ length: totalCount }, (_, i) => { - const dtoNum = totalCount - i; - const wholField = `yy${dtoNum}PrlgWhol`; - const lyngField = `yy${dtoNum}LyngPrlg`; - const dmltnField = `yy${dtoNum}DmltnPrlg`; + ...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX }, (_, i) => { + const yyIdx = i + 1; + const wholField = `yy${yyIdx}PrlgWhol`; + const lyngField = `yy${yyIdx}LyngPrlg`; + const dmltnField = `yy${yyIdx}DmltnPrlg`; return [ { name: wholField, @@ -687,13 +685,18 @@ const rawSewagePipeTotList = [ header: { text: "총계" }, items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"], }, - ...Array.from({ length: totalCount }, (_, i) => { - const dtoNum = totalCount - i; - const yearLabel = i === 0 ? "1996년이전" : `${startYear + (i - 1)}년`; + { + name: "year1996Before", + header: { text: "1996년이전" }, + items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"], + }, + ...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX - 1 }, (_, i) => { + const yyIdx = i + 2; + const year = 1994 + yyIdx; return { - name: `group_${yearLabel}`, - header: { text: yearLabel }, - items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`], + name: `group_yy${yyIdx}`, + header: { text: `${year}년` }, + items: [`yy${yyIdx}PrlgWhol`, `yy${yyIdx}LyngPrlg`, `yy${yyIdx}DmltnPrlg`], }; }), ], diff --git a/apps/st/src/components/util/column/total/sewagePipeMaintain/PipeDredgingStatusTotColumn.ts b/apps/st/src/components/util/column/total/sewagePipeMaintain/PipeDredgingStatusTotColumn.ts new file mode 100644 index 000000000..aec00f860 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewagePipeMaintain/PipeDredgingStatusTotColumn.ts @@ -0,0 +1,79 @@ +import { ValueType } from "realgrid"; + +// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 2. 관로준설 +export const PipeDredgingStatusTotColumn = { + columns: [ + // 행정구역 + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' } }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' } }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' } }, + + // 하수관로 총 시설연장 + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로총시설연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + + // 준설연장 및 개소수 > 관로(m) + { name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 준설연장 및 개소수 > 관로(m) > 분류식 + { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 준설연장 및 개소수 > 맨홀, 오·우수받이, 토실·토구 + { name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'stofcbrOtflCnt', fieldName: 'stofcbrOtflCnt', header: { text: '토실·토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 계획 & 비율 + { name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획 연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '총시설연장대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 준설실적 + { name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drdgnQnt', fieldName: 'drdgnQnt', header: { text: '준설량(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 준설토사처리 + { name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설토처리비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계(톤)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양투기' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타(소각, 고형화, 시멘트원료 등)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + ].map((col) => ({ ...col, editable: false })), + columnLayout: [ + { name: 'group_admin', header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'admdstNm'] }, + 'wholFcltPrlg', + { + name: 'group_dredge_info', + header: { text: '준설연장 및 개소수' }, + items: [ + { + name: 'group_pipe', + header: { text: '관로(m)' }, + items: [ + 'wholSwagePpl', 'cbsysPplPrlg', + { name: 'group_sep', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] } + ] + }, + 'mnhlCnt', 'exlnDrtwRcvprpCnt', 'stofcbrOtflCnt' + ] + }, + 'drdgnPlanPrlg', 'drdgnTrgtCtrsDrdgnRt', 'wholPrlgCtrsDrdgnRt', + { + name: 'group_perf', + header: { text: '준설실적' }, + items: ['drdgnAmt', 'drdgnQnt'] + }, + { + name: 'group_soil_treat', + header: { text: '준설토사처리' }, + items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt'] + }, + ], + rowStartNum: 4, + fixedColumns: 1, + tabKey: 'ST00030002', + tabLabel: '관로준설' +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewagePipeMaintain/RepairStatusTotColumn.ts b/apps/st/src/components/util/column/total/sewagePipeMaintain/RepairStatusTotColumn.ts new file mode 100644 index 000000000..307253309 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewagePipeMaintain/RepairStatusTotColumn.ts @@ -0,0 +1,112 @@ +import { ValueType } from "realgrid"; + +// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 1. 개보수 +export const RepairStatusTotColumn = { + columns: [ + // 행정구역 + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false }, + + // 개·보수관로 > 전체보수(m) + { name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 개·보수관로 > 전체보수(m) > 분류식 + { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + // 개·보수관로 > 부분보수(개소) + { name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 개·보수관로 > 부분보수(개소) > 분류식 + { name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 맨홀(개소) + { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 맨홀(개소) > 분류식 + { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 받이 + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 토실, 토구 + { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 수밀검사CCTV조사량(m) + { name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·CCTV조사량(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + + ].map((col) => ({ ...col, editable: false })), + columnLayout: [ + { + name: 'group_admin', + header: { text: '행정구역' }, + items: ['ctpvNm', 'sggNm', 'admdstNm'] + }, + { + name: 'group_repair_main', + header: { text: '개·보수관로' }, + items: [ + { + name: 'group_full_repair', + header: { text: '전체보수(m)' }, + items: [ + 'mendngWhol', + 'cbsysMendngPrlg', + { + name: 'group_full_sep', + header: { text: '분류식' }, + items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] + } + ] + }, + { + name: 'group_part_repair', + header: { text: '부분보수(개소)' }, + items: [ + 'partMendngWhol', + 'cbsysPartMendngPrlg', + { + name: 'group_part_sep', + header: { text: '분류식' }, + items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg'] + } + ] + } + ] + }, + { + name: 'group_manhole', + header: { text: '맨홀(개소)' }, + items: [ + 'cbsysMnhlCnt', + { + name: 'group_mh_sep', + header: { text: '분류식' }, + items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] + } + ] + }, + { + name: 'group_inlet', + header: { text: '받이' }, + items: ['drtwRcvprpCnt', 'exlnRcvprpCnt'] + }, + { + name: 'group_chamber', + header: { text: '토실,토구' }, + items: ['stofcbrCnt', 'otflCnt'] + }, + 'cctvExmnPrlg', + ], + rowStartNum: 4, + fixedColumns: 1, + tabKey: 'ST00030001', + tabLabel: '개보수' +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn.ts b/apps/st/src/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn.ts new file mode 100644 index 000000000..f8e13699c --- /dev/null +++ b/apps/st/src/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn.ts @@ -0,0 +1,12 @@ +import { PipeDredgingStatusTotColumn } from "./PipeDredgingStatusTotColumn" +import { RepairStatusTotColumn } from "./RepairStatusTotColumn" + +const sewagePipeMaintainColumn = [ + RepairStatusTotColumn, + PipeDredgingStatusTotColumn +]; + +export const SewagePipeMaintainColumnListTot = sewagePipeMaintainColumn.map((tab) => ({ + ...tab, + columns: tab.columns.map((col) => ({ ...col, editable: false as const })), +})); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index 02d0144c4..77572d361 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -3,6 +3,8 @@ import { stClient } from "@/shared/api/client"; import type { SeweragePipeFacilityAdminAreaListRequest, SeweragePipeFacilityAdminAreaListResponse, + SeweragePipeFactConnectionSaveRequest, + SeweragePipeFactConnectionSaveResponse, SeweragePipeFacilitiesListRequest, SeweragePipeFacilitiesListResponse, SeweragePipeInputConnectionStatusResponse, @@ -12,9 +14,13 @@ import type { SeweragePipeInputPipeTypeResponse, SeweragePipeInputRegionalFactStatusSaveRequest, SeweragePipeInputRegionalFactStatusSaveResponse, + SeweragePipeInputRegionalFactStatusSubmitRequest, + SeweragePipeInputRegionalFactStatusSubmitResponse, SeweragePipeInputRegionalResponse, SeweragePipeRequestParams, StatisticsInputSewPipeService, + SeweragePipeInputFactInstallStatusSaveRequest, + SeweragePipeInputFactInstallStatusSaveResponse, } from "../../model/input/statistics-sewage-pipe-input-interface"; export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeService { @@ -82,6 +88,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS return response.data; } + public async saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/save`, + param, + ); + return response.data; + } + public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`, @@ -89,6 +103,22 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS ); return response.data; } + + public async submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/submit`, + param, + ); + return response.data; + } + + public async saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/save`, + param, + ); + return response.data; + } }; export const statisticsInputSewPipeApiService = new StatisticsInputSewPipeApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/statistics-total-service.ts b/apps/st/src/entities/statistics/api/statistics-total-service.ts index cc2487261..402fd016c 100644 --- a/apps/st/src/entities/statistics/api/statistics-total-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-total-service.ts @@ -2,7 +2,18 @@ import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; import type { SewerageSupplyRateRequest, SewerageSupplyRateResponse } from "../model/statistics-total/statistics-sewage-supply-rate-total-interface"; -import type { SewagePipeTotalRequest, SewagePipeTotalResponse } from "../model/statistics-total/statistics-sewage-pipe-total-interface"; +import type { + SewagePipeMaintainDredgingTotalResponse, + SewagePipeMaintainTotalRequest, + SewagePipeMaintainRepairTotalResponse, +} from "../model/statistics-total/statistics-sewage-pipe-maintain-total-interface"; +import type { + SewagePipeFactInstallStatusResponse, + SewagePipePipeDiamStatusResponse, + SewagePipePipeTypeFacilityStatusResponse, + SewagePipeTotalRequest, + SewagePipeTotalResponse, +} from "../model/statistics-total/statistics-sewage-pipe-total-interface"; import type { SewerageServiceLevelComplaintResponse, SewerageServiceLevelRequest, @@ -41,6 +52,60 @@ export class StatisticsTotalApiService { ); return response.data; } + + public async getSewagePipeFactInstallStatus( + params: SewagePipeTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/facility-installation-status`, + { params }, + ); + return response.data; + } + + /** ST00020003 — 관종·관경별 시설현황(합계) */ + public async getSewagePipePipeTypeFacilityStatus( + params: SewagePipeTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-type-facility-status`, + { params }, + ); + return response.data; + } + + /** ST00020004 — 관경별 시설현황(합계) */ + public async getSewagePipePipeDiamStatus( + params: SewagePipeTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-diam-status`, + { params }, + ); + return response.data; + } + + /** ST00030001 — 하수관로 유지관리 > 개보수(합계) */ + public async getSewagePipeMaintainRepairTotal( + params: SewagePipeMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/repair`, + { params }, + ); + return response.data; + } + + /** ST00030002 — 하수관로 유지관리 > 관로준설(합계) */ + public async getSewagePipeMaintainDredgingTotal( + params: SewagePipeMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/dredging`, + { params }, + ); + return response.data; + } } export const statisticsTotalStatusApi = new StatisticsTotalApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index 299078872..24264cc73 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -22,6 +22,10 @@ export interface SeweragePipeInputStatusInfo { export interface VerifyResultItem { vrfcCd: string; rsnExpln: string; + /** 현재값(문자열로 내려오는 경우가 많음) */ + nowVlCn?: string; + /** 비교값 */ + compVlCn?: string; } /** 필드명 -> 검증 결과 배열 */ @@ -106,6 +110,12 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit< "verificationResult" >; +/** Tab2 저장 — saveList */ +export type SeweragePipeInputFactInstallStatusSaveItem = Omit< + SeweragePipeInputFactInstallStatusItem, + "verificationResult" +>; + /** 지역별 시설현황 저장 API 요청 */ export interface SeweragePipeInputRegionalFactStatusSaveRequest { statsYr: string; @@ -118,7 +128,50 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest { } export interface SeweragePipeInputRegionalFactStatusSaveResponse { - data: string; + data?: string; + message?: string; + status: string; +} + +/** 지역별 시설현황 제출 API 요청 (save item 구조 재사용) */ +export interface SeweragePipeInputRegionalFactStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + submitList: SeweragePipeInputRegionalFactStatusSaveItem[]; +} + +export interface SeweragePipeInputRegionalFactStatusSubmitResponse { + data: { + finalStatus: string; + nvpassed: boolean; + } + message?: string; + status: string; +} + +export interface SeweragePipeInputFactInstallStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin: string; + saveList: SeweragePipeInputFactInstallStatusSaveItem[]; +} + +export interface SeweragePipeInputFactInstallStatusSaveResponse { + data?: string; message?: string; status: string; } @@ -128,7 +181,7 @@ export interface SeweragePipeInputFactInstallStatusItem { admdstCd: string; sggNm: string; admdstNm: string; - prcsFcltSn: number; + fcltSn: string; swtrmtFcltCd: string; swtrmtFcltNm: string; prcsFcltCpct: number; @@ -425,9 +478,9 @@ export interface SeweragePipeInputManholeResponse { /** 시설별 현황 관리 - 하수처리장별 연결 현황 목록 항목 */ export interface SeweragePipeInputFacilityConnectionItem { - bscFctNm: string; - fctCpc: number; - bscFctCd: string; + fcltNm: string; + fcltCpct: number; + fcltSn: string; isConnection: string; connectionAreaList: string; } @@ -472,9 +525,10 @@ export interface SeweragePipeFacilitiesListItem { ctpvNm: string; sggNm: string; admdstCd: string; - bscFctNm: string; - fctCpc: number; - bscFctCd: string; + fcltNm: string; + fcltCpct: number; + fcltCd: string; + fcltSn: string; } /** facilitiesList API 응답 */ @@ -490,7 +544,7 @@ export interface SeweragePipeFacilityAdminAreaListRequest { swtrmtFcltCd: string; sewerBzmnCd: string; ctpvNm: string; - sggNm: string; + sggNm?: string; } /** 처리시설별 행정구역 목록 항목 */ @@ -509,6 +563,34 @@ export interface SeweragePipeFacilityAdminAreaListResponse { message?: string; } +/** 시설별 연결관계 저장 — saveList 항목 */ +export interface SeweragePipeFactConnectionSaveItem { + admdstCd: string; + admdstNm: string; +} + +/** 시설별 연결관계 저장 요청 */ +export interface SeweragePipeFactConnectionSaveRequest { + ctpvNm: string; + sggNm: string; + fcltSn: string; + statsYr: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + prcsFcltCpct: number; + sewerBzmnCd: string; + userId: string; + saveList: SeweragePipeFactConnectionSaveItem[]; + deleteList: string[]; +} + +/** 시설별 연결관계 저장 응답 */ +export interface SeweragePipeFactConnectionSaveResponse { + status: string; + data?: string; + message?: string; +} + export interface StatisticsInputSewPipeService { getRegionalFactStatus(param: SeweragePipeRequestParams): Promise; getFactInstallStatus(param: SeweragePipeRequestParams): Promise; @@ -518,7 +600,8 @@ export interface StatisticsInputSewPipeService { getConnectionStatus(param: SeweragePipeRequestParams): Promise; getFacilitiesList(param: SeweragePipeFacilitiesListRequest): Promise; getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise; - saveRegionalFactStatus( - param: SeweragePipeInputRegionalFactStatusSaveRequest, - ): Promise; + saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise; + saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise; + submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise; + saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts index 1c14dfab8..f88e9a08f 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts @@ -167,11 +167,61 @@ export interface SewerageSupplyRateSubmitResponse { status: string; } -// 하수도 통계 입력 기간 조회 +// 하수도 통계 입력 기간 조회 (입력 INPT / 검증 VRFC 구간) +export interface CheckInputPeriodItem { + statsYr: string; + seCd: number; + seType: string; + bgngYmd: string; + endYmd: string; + current: boolean; +} + export interface CheckInputPeriodResponse { status: string; message: string; - data: boolean; + data: CheckInputPeriodItem[]; +} + +/** 입력기간(INPT) 중 현재 구간이면 true */ +export function isInputPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean { + return (periods ?? []).some((p) => p.seType === "INPT" && p.current === true); +} + +/** `YYYYMMDD` → `yyyy-mm-dd` (그 외 형식은 그대로 반환) */ +export function formatCheckInputYmdAsDash(ymd: string | null | undefined): string { + if (ymd == null || ymd === "") return ""; + if (ymd.length === 8 && /^\d{8}$/.test(ymd)) { + return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`; + } + return ymd; +} + +/** 검증기간(VRFC) 중 `current`인 항목 (없으면 null) */ +export function getCurrentVerificationPeriod( + periods: CheckInputPeriodItem[] | null | undefined, +): CheckInputPeriodItem | null { + return (periods ?? []).find((p) => p.seType === "VRFC" && p.current === true) ?? null; +} + +/** 검증기간(VRFC) 중 현재 구간이면 true — 저장·제출·제출 취소 불가 */ +export function isVerificationPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean { + return getCurrentVerificationPeriod(periods) !== null; +} + +/** + * 검증기간이면 차단 안내 문구(줄바꿈 + yyyy-mm-dd ~ yyyy-mm-dd), 아니면 null + */ +export function getVerificationPeriodActionBlockedMessage( + periods: CheckInputPeriodItem[] | null | undefined, +): string | null { + const v = getCurrentVerificationPeriod(periods); + if (!v) return null; + const from = formatCheckInputYmdAsDash(v.bgngYmd); + const to = formatCheckInputYmdAsDash(v.endYmd); + const range = + from && to ? `${from} ~ ${to}` : [from, to].filter(Boolean).join(" ~ "); + return `검증기간에는 저장, 제출, 제출 취소를 할 수 없습니다.\n검증기간: ${range}`; } export interface StatisticsInputSewSupplyRateService { diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts new file mode 100644 index 000000000..139320868 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts @@ -0,0 +1,77 @@ +export interface SewagePipeMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; +} + +export interface SewagePipeMaintainRepairTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + rowType: string; + sewerBzmnCd: string | null; + admdstNm: string; + mendngWhol: number; + cbsysMendngPrlg: number; + sprsysDrtwPrlg: number; + sprsysExlnPrlg: number; + partMendngWhol: number; + cbsysPartMendngPrlg: number; + sprsysDrtwMendngPrlg: number; + sprsysExlnMendngPrlg: number; + cbsysMnhlCnt: number; + sprsysDrtwMnhlCnt: number; + sprsysExlnMnhlCnt: number; + drtwRcvprpCnt: number; + exlnRcvprpCnt: number; + stofcbrCnt: number; + otflCnt: number; + cctvExmnPrlg: number; + rmrk: string | null; +} + +export interface SewagePipeMaintainRepairTotalResponse { + status: string; + message?: string; + data: SewagePipeMaintainRepairTotalItem[]; +} + +/** `GET .../sewer-pipe-mtc/dredging` 목록 항목 */ +export interface SewagePipeMaintainDredgingTotalItem { + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + statsYr: string; + rowType: string; + sewerBzmnCd: string | null; + wholFcltPrlg: number; + wholSwagePpl: number; + cbsysPplPrlg: number; + sprsysDrtwPrlg: number; + sprsysExlnPrlg: number; + mnhlCnt: number; + exlnDrtwRcvprpCnt: number; + stofcbrOtflCnt: number; + drdgnPlanPrlg: number; + drdgnTrgtCtrsDrdgnRt: number; + wholPrlgCtrsDrdgnRt: number; + drdgnAmt: number; + drdgnQnt: number; + drdgnUntprcAmt: number; + wholDrdgnPrcsQnt: number; + rclmQnt: number; + oceanEmsnQnt: number; + rcycQnt: number; + etcQnt: number; + rmrk: string | null; +} + +/** `GET .../sewer-pipe-mtc/dredging` 응답 */ +export interface SewagePipeMaintainDredgingTotalResponse { + status: string; + message?: string; + data: SewagePipeMaintainDredgingTotalItem[]; +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts index f5c71974b..d9a7825cf 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts @@ -57,3 +57,180 @@ export interface SewagePipeTotalResponse { message: string; data: SewagePipeTotalItem[]; } + +export interface SewagePipeFactInstallStatusItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstCd: string; + admdstNm: string; + swtrmtFcltNm: string; + prcsFcltCpct: number | null; + rowType: string; + prcsZoneArea: number; + cbsysPrcsZoneArea: number; + sprsysPrcsZoneArea: number; + sprsysVartnRt: number; + mltvEqpmCnt: number; + cbsysMltvEqpmCnt: number; + sprsysMltvEqpmCnt: number; + sprsysMltvLnkgCnt: number; + sprsysMltvYetLnkgCnt: number; + planPrcsZoneArea: number; + cbsysPlanPrcsZoneArea: number; + sprsysPlanPrcsZoneArea: number; + wholFcltPrlg: number; + wholIcptgAtlfcPrlg: number | null; + icptgPrlg: number; + atlfcsPrlg: number; + cbsysStofcbrCnt: number; + sprsysStofcbrCnt: number; + sprsysExlnOtflCnt: number; + wholBrchlnPrlg: number; + cbsysBrchlnPrlg: number; + exlnBrchlnPrlg: number; + drtwBrchlnPrlg: number; +} + +export interface SewagePipeFactInstallStatusResponse { + status: string; + message?: string; + data: SewagePipeFactInstallStatusItem[]; +} + +/** ST00020003 합계 — `GET .../sewer-pipe/pipe-type-facility-status` */ +export interface SewagePipePipeTypeFacilityStatusItem { + pipeByPrstWhol: number; + lyngPrlg: number; + dmltnPrlg: number; + yy1PrlgWhol: number; + yy1LyngPrlg: number; + yy1DmltnPrlg: number; + yy2PrlgWhol: number; + yy2LyngPrlg: number; + yy2DmltnPrlg: number; + yy3PrlgWhol: number; + yy3LyngPrlg: number; + yy3DmltnPrlg: number; + yy4PrlgWhol: number; + yy4LyngPrlg: number; + yy4DmltnPrlg: number; + yy5PrlgWhol: number; + yy5LyngPrlg: number; + yy5DmltnPrlg: number; + yy6PrlgWhol: number; + yy6LyngPrlg: number; + yy6DmltnPrlg: number; + yy7PrlgWhol: number; + yy7LyngPrlg: number; + yy7DmltnPrlg: number; + yy8PrlgWhol: number; + yy8LyngPrlg: number; + yy8DmltnPrlg: number; + yy9PrlgWhol: number; + yy9LyngPrlg: number; + yy9DmltnPrlg: number; + yy10PrlgWhol: number; + yy10LyngPrlg: number; + yy10DmltnPrlg: number; + yy11PrlgWhol: number; + yy11LyngPrlg: number; + yy11DmltnPrlg: number; + yy12PrlgWhol: number; + yy12LyngPrlg: number; + yy12DmltnPrlg: number; + yy13PrlgWhol: number; + yy13LyngPrlg: number; + yy13DmltnPrlg: number; + yy14PrlgWhol: number; + yy14LyngPrlg: number; + yy14DmltnPrlg: number; + yy15PrlgWhol: number; + yy15LyngPrlg: number; + yy15DmltnPrlg: number; + yy16PrlgWhol: number; + yy16LyngPrlg: number; + yy16DmltnPrlg: number; + yy17PrlgWhol: number; + yy17LyngPrlg: number; + yy17DmltnPrlg: number; + yy18PrlgWhol: number; + yy18LyngPrlg: number; + yy18DmltnPrlg: number; + yy19PrlgWhol: number; + yy19LyngPrlg: number; + yy19DmltnPrlg: number; + yy20PrlgWhol: number; + yy20LyngPrlg: number; + yy20DmltnPrlg: number; + yy21PrlgWhol: number; + yy21LyngPrlg: number; + yy21DmltnPrlg: number; + yy22PrlgWhol: number; + yy22LyngPrlg: number; + yy22DmltnPrlg: number; + yy23PrlgWhol: number; + yy23LyngPrlg: number; + yy23DmltnPrlg: number; + yy24PrlgWhol: number; + yy24LyngPrlg: number; + yy24DmltnPrlg: number; + yy25PrlgWhol: number; + yy25LyngPrlg: number; + yy25DmltnPrlg: number; + yy26PrlgWhol: number; + yy26LyngPrlg: number; + yy26DmltnPrlg: number; + yy27PrlgWhol: number; + yy27LyngPrlg: number; + yy27DmltnPrlg: number; + yy28PrlgWhol: number; + yy28LyngPrlg: number; + yy28DmltnPrlg: number; + yy29PrlgWhol: number; + yy29LyngPrlg: number; + yy29DmltnPrlg: number; + yy30PrlgWhol: number; + yy30LyngPrlg: number; + yy30DmltnPrlg: number; + ctpvNm: string; + sggNm: string | null; + admdstCd: string; + admdstNm: string; + swrgPipeNm: string; + statsYr: string; + rowType: string; + rmrk: string | null; +} + +export interface SewagePipePipeTypeFacilityStatusResponse { + status: string; + message?: string; + data: SewagePipePipeTypeFacilityStatusItem[]; +} + +/** ST00020004 합계 — `GET .../sewer-pipe/pipe-diam-status` */ +export interface SewagePipePipeDiamStatusItem { + ctpvNm: string; + sggNm: string | null; + admdstCd: string; + admdstNm: string; + statsYr: string; + swrgPipeNm: string; + rowType: string; + pdmtByTsmWhol: number; + step1PdmtPrlg: number; + step2PdmtPrlg: number; + step3PdmtPrlg: number; + step4PdmtPrlg: number; + step5PdmtPrlg: number; + sqrePdmtPrlg: number; + etcPdmtPrlg: number; + rmrk: string | null; +} + +export interface SewagePipePipeDiamStatusResponse { + status: string; + message?: string; + data: SewagePipePipeDiamStatusItem[]; +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-supply-rate-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-supply-rate-total-interface.ts index 31f7b81db..faef3a3f1 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-supply-rate-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-supply-rate-total-interface.ts @@ -4,6 +4,8 @@ export interface SewerageSupplyRateRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } export interface SewerageSupplyRateTotalItem { From 9fc2fc894203a9b5bb3a57d38e72f7be953bf191 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 29 Apr 2026 11:37:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[st]=20feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/statistics/Dashboard.tsx | 15 +- .../pages/statistics/model/panel-config.tsx | 16 +- .../statistics/ui/panels/notice-panel.tsx | 31 +-- .../ui/panels/schedule-calendar-panel.tsx | 19 +- .../ui/panels/schedule-info-panel.tsx | 133 +++++++++---- .../ui/panels/stats-submission-panel.tsx | 185 +++++++++++++----- .../statistics/ui/panels/unsubmit-panel.tsx | 17 +- .../statistics/ui/panels/vrfc-err-panel.tsx | 8 +- .../ui/panels/vrfc-report-panel.tsx | 87 +++++--- .../pages/statistics/ui/statistics-page.tsx | 4 +- 10 files changed, 348 insertions(+), 167 deletions(-) diff --git a/apps/st/src/components/statistics/Dashboard.tsx b/apps/st/src/components/statistics/Dashboard.tsx index b3734e386..423294d2f 100644 --- a/apps/st/src/components/statistics/Dashboard.tsx +++ b/apps/st/src/components/statistics/Dashboard.tsx @@ -63,9 +63,6 @@ function Dashboard() { items={panels.map((p) => p.id)} strategy={rectSortingStrategy} > - {/* TBD: 그리드 레이아웃 비율 정의 필요 - 현재: 3열 그리드, 반응형 브레이크포인트 적용 - SPEC 레이아웃에 따라 조정 필요 */}
{panels.map((panel) => ( ) : (
- - - - - - + + + + + +
)} diff --git a/apps/st/src/pages/statistics/model/panel-config.tsx b/apps/st/src/pages/statistics/model/panel-config.tsx index 75f678a7f..8cc4f6271 100644 --- a/apps/st/src/pages/statistics/model/panel-config.tsx +++ b/apps/st/src/pages/statistics/model/panel-config.tsx @@ -6,7 +6,7 @@ import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel"; import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel"; import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel"; import { UnSubmitPanel } from "../ui/panels/unsubmit-panel"; -import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel"; +// import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel"; import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel"; import type { PanelItem } from "./types"; @@ -36,23 +36,23 @@ export function getInitialPanels({ component: , className: "", }, + // { + // id: "panel-d", + // component: , + // className: "", + // }, { id: "panel-d", - component: , - className: "", - }, - { - id: "panel-e", component: , className: "", }, { - id: "panel-f", + id: "panel-e", component: , className: "", }, { - id: "panel-g", + id: "panel-f", component: , className: "", }, diff --git a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx index 8580ce2f0..2d182b741 100644 --- a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx @@ -17,7 +17,7 @@ export function NoticePanel() { const recentPostListQuery = useRecentPostList({ page: 0, - size: 3, + size: 4, keywordType: "bbsTypeCd", keyword: "BBSNOTICE", }); @@ -45,14 +45,18 @@ export function NoticePanel() { }); return ( -
+
-

공지사항

-
@@ -67,17 +71,16 @@ const noticeColumns = [ columnHelper.accessor("pstTtl", { header: "제목", cell: ({ row }) => ( -
-
- {row.original.pstTtl} -
-
- {row.original.wrtrNm} - {formatDate(row.original.regDt)} -
-
+
{row.original.pstTtl}
), }), + columnHelper.accessor("wrtrNm", { + header: "작성자", + }), + columnHelper.accessor("regDt", { + header: "등록일", + cell: ({ row }) => formatDate(row.original.regDt), + }), ]; function formatDate(value: string) { diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx index 4bc29a441..8f85b1351 100644 --- a/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx @@ -8,6 +8,7 @@ import dayjs from "dayjs"; import { SITE_ID } from "@/shared/config"; import { useSchedules } from "@/entities/schedule"; +import { LoadingWithAnimation } from "@common/components"; import { useDragHandle } from "../sortable-panel"; @@ -31,21 +32,24 @@ export function ScheduleCalendarPanel() { }); return ( -
+
-

일정관리

+

일정관리

-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {data && ( -
- {/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */} +
diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx index 7d2ab0eb6..8c162a436 100644 --- a/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx @@ -1,7 +1,14 @@ "use client"; +import dayjs from "dayjs"; + import type { ScheduleInfoSearchParams } from "@/entities/statistics"; import { useScheduleInfo } from "@/entities/statistics"; +import { + Badge, + type BadgeProps, + LoadingWithAnimation, +} from "@common/components"; import { useDragHandle } from "../sortable-panel"; @@ -9,63 +16,121 @@ interface ScheduleInfoPanelProps { params: ScheduleInfoSearchParams | null; } +const CURRENT_STATUS_LABEL_MAP = { + INPT: "입력중", + VRFC: "검증중", + NONE: "기간외", +}; + +const CURRENT_STATUS_COLOR_MAP = { + INPT: "", + VRFC: "gray", + NONE: "lightGray", +}; + export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) { const { dragHandleProps, isDragging } = useDragHandle(); const { data, isLoading, error } = useScheduleInfo(params); return ( -
+
-

일정안내

+

일정안내

+ {data && ( +
+ + {data.ddayLabel} + + + {data.dday} + +
+ )}
-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {error && ( -
+
데이터를 불러올 수 없습니다.
)} {data && ( -
- {/* D-day 표시 */} -
-
- {data.ddayLabel} -
-
- {data.dday} -
-
- {data.currentSe} -
-
- - {/* TBD: 일정 목록 표시 형식 정의 필요 */} -
-
-
- 입력기간 -
-
- {data.inptBgngYmd} ~ {data.inptEndYmd} + <> +
+
+

+ 이번 주 주요 일정 +

+
+ + 현재 진행 상태 + + + { + CURRENT_STATUS_LABEL_MAP[ + data.currentSe as keyof typeof CURRENT_STATUS_LABEL_MAP + ] + } +
-
-
- 검증기간 +
+
+ + {data.inptSeCd}차 입력기간 + +

+ {dayjs(data.inptBgngYmd).format( + "YYYY.MM.DD", + )}{" "} + ~{" "} + {dayjs(data.inptEndYmd).format( + "YYYY.MM.DD", + )} +

-
- {data.vrfcBgngYmd} ~ {data.vrfcEndYmd} +
+ + {data.vrfcSeCd}차 검증기간 + +

+ {dayjs(data.vrfcBgngYmd).format( + "YYYY.MM.DD", + )}{" "} + ~{" "} + {dayjs(data.vrfcEndYmd).format( + "YYYY.MM.DD", + )} +

-
+ )}
diff --git a/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx index 23fe0c963..f973f4e9e 100644 --- a/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx @@ -1,8 +1,14 @@ "use client"; -import { useDashboardStatsSubmissionRate } from "@/entities/statistics"; +import { Frown, Laugh, Meh, OctagonAlert, OctagonX, Smile } from "lucide-react"; + +import { + useDashboardEcptReqCount, + useDashboardStatsSubmissionRate, + useDashboardVrfcErrCount, +} from "@/entities/statistics"; import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; -import { LoadingWithAnimation } from "@common/components"; +import { LoadingWithAnimation, Progress } from "@common/components"; import { useDragHandle } from "../sortable-panel"; @@ -10,70 +16,147 @@ interface StatsSubmissionPanelProps { params: DashboardSearchParams | null; } +const STATUS_LEVEL_TEXT_COLOR = { + GREAT: "text-blue-500", + GOOD: "text-green-500", + WARNING: "text-yellow-500", + BAD: "text-red-500", +}; + +const STATUS_LEVEL_EMOJI = { + GREAT: , + GOOD: , + WARNING: , + BAD: , +}; + export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) { const { dragHandleProps, isDragging } = useDragHandle(); const { data, isLoading, error } = useDashboardStatsSubmissionRate(params); + const vrfcErrQuery = useDashboardVrfcErrCount(params); + const ecptReqQuery = useDashboardEcptReqCount(params); return ( -
-
+ {/*
-

통계입력현황

-
-
- {isLoading && ( -
-
- -
-
- )} - {error && ( -
- 데이터를 불러올 수 없습니다. -
- )} - {data && ( -
- {/* TBD: statusLevel별 이모지 매핑 규칙 필요 */} -
-
- {/* TBD: statusLevel 값에 따른 이모지 표시 */} - {data.statusLevel === "GOOD" - ? "😊" - : data.statusLevel === "WARNING" - ? "⚠️" - : "📊"} +

통계입력현황

+
*/} +
+
+
+

통계입력현황

+ {isLoading && ( +
+
+ +
-
- {data.statusLevel} + )} + {error && ( +
+ 데이터를 불러올 수 없습니다.
-
+ )} +
+ {data && ( +
+ {/* 통계제출률 상태 이모지 */} +
+ { + STATUS_LEVEL_EMOJI[ + data.statusLevel as keyof typeof STATUS_LEVEL_EMOJI + ] + } +
- {/* 통계제출률 프로그레스바 */} -
-
- 통계제출률 - - {data.submissionRate?.toFixed(1)}% - + {/* 통계제출률 프로그레스바 */} +
+
+ + 통계제출률 + + + {data.submissionRate?.toFixed( + 1, + )} + % + + ({data.submittedCount}/ + {data.totalCount}) + + +
+ +
+
+ )} +
+
+
+

+ 검증오류 및 예외 +

+
+ {/* 검증오류 수 카드 */} +
+
+ + 검증오류 수 +
+ {vrfcErrQuery.isLoading && ( +
-
+ )} + {vrfcErrQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {vrfcErrQuery.data && ( +
+ {vrfcErrQuery.data.vrfcErrCount.toLocaleString()} +
+ )}
-
-
-
-
- 제출: {data.submittedCount} - 전체: {data.totalCount} + + {/* 예외신청 수 카드 */} +
+
+ + 예외신청 수 +
+ {ecptReqQuery.isLoading && ( +
-
+ )} + {ecptReqQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {ecptReqQuery.data && ( +
+ {ecptReqQuery.data.ecptReqCount.toLocaleString()} +
+ )}
- )} +
); diff --git a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx index d8c1fc832..fae349694 100644 --- a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx @@ -15,23 +15,20 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) { const { data, isLoading, error } = useDashboardUnSubmitCount(params); return ( -
+
-

미제출 지표

+

미제출 지표

{data && ( - + {data.unSubmitCount}건 )}
-
+
{isLoading && (
@@ -51,11 +48,11 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) { 미제출 지표가 없습니다.
) : ( -
+
{data.unSubmitList.map((item) => (

{item.statsNm} diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx index 2a753196c..58fb11477 100644 --- a/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx @@ -20,15 +20,15 @@ export function VrfcErrPanel({ params }: VrfcErrPanelProps) { const ecptReqQuery = useDashboardEcptReqCount(params); return ( -

+
-

검증오류 및 예외

+

검증오류 및 예외

-
+
{/* 검증오류 수 카드 */}
diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx index 193e71a37..51834d857 100644 --- a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx @@ -2,6 +2,11 @@ import { useDashboardVrfcReport } from "@/entities/statistics"; import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface"; +import { + Badge, + type BadgeProps, + LoadingWithAnimation, +} from "@common/components"; import { useDragHandle } from "../sortable-panel"; @@ -9,79 +14,107 @@ interface VrfcReportPanelProps { params: DashboardSearchParams | null; } +const TYPE_CODE_NAME_MAP: Record = { + NV: "일반", + IMR: "I-MR", +}; + +const TYPE_CODE_COLOR_MAP: Record = { + NV: "gray", + IMR: "red", +}; + export function VrfcReportPanel({ params }: VrfcReportPanelProps) { const { dragHandleProps, isDragging } = useDragHandle(); const { data, isLoading, error } = useDashboardVrfcReport(params); return ( -
+
-

이상치 선별보고서

+

이상치 선별보고서

-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {error && ( -
+
데이터를 불러올 수 없습니다.
)} {data && ( -
+
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */} {data.typeCounts && Object.keys(data.typeCounts).length > 0 && (
{Object.entries(data.typeCounts).map( ([type, count]) => ( - - {type}: {count} - + {TYPE_CODE_NAME_MAP[type]}:{" "} + {count} + ), )}
)} {/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */} -
+
{data.list.length === 0 ? ( -
+
이상치 항목이 없습니다.
) : ( -
+
{data.list.map((item, index) => (
-
-
+
+ {item.statsNm} -
- {/* TBD: vrfcCd 값별 표시 레이블 */} - - {item.vrfcCd} + + { + TYPE_CODE_NAME_MAP[ + item.vrfcCd + ] + } +
-
+

{item.rsnExpln} -

-
+

+
{item.admdstNm} {/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */} - 현재: {item.nowVlCn} / 비교:{" "} - {item.compVlCn} + 현재: {item.nowVlCn ?? "-"}{" "} + / 비교:{" "} + {item.compVlCn ?? "-"}
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */} diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index e50f90a0d..131ab95dd 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -37,14 +37,14 @@ const StatisticsPage = () => { // 탭이 없으면 Dashboard 표시 if (tabs.length === 0) { return ( -
+
); } return ( -
+
{tabs.map((tab) => { // 렌더링 시점에 컴포넌트가 없으면 바로 로드 if (!componentCache.current.has(tab.id)) {