From 4c7ff53d78e6bc3ec5268b6e0ace6a1a9bb424bf Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:43:03 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]03.?= =?UTF-8?q?=ED=95=98=EC=88=98=EA=B4=80=EB=A1=9C=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/total/SewagePipeMaintainTot.tsx | 9 +++++++-- .../statistics-sewage-pipe-maintain-total-interface.ts | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx index a061146c5..b3dfe7a46 100644 --- a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx @@ -15,12 +15,17 @@ import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001"; function buildSewagePipeMaintainTotSearchParam(values: SearchFormValues): SewagePipeMaintainTotalRequest { + const isSummaryEnabled = values.isSummary !== 'ALL'; + const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined; + return { 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 } : {}), }; } @@ -128,7 +133,7 @@ const SewagePipeMaintainTot = () => { return (
- + {isLoading && (
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 index 139320868..ca2c79b14 100644 --- 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 @@ -4,6 +4,8 @@ export interface SewagePipeMaintainTotalRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } export interface SewagePipeMaintainRepairTotalItem { From 71077f4c8e0428c6545db039b3eec3f776e7b815 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:47:11 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]04.?= =?UTF-8?q?=ED=95=98=EC=88=98=EC=A0=80=EB=A5=98=EC=8B=9C=EC=84=A4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=97=EB=AC=BC=20=ED=8E=8C=ED=94=84=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/SewageReservoirPumpTot.tsx | 186 +++++++++++++++++- .../SewerPumpStationStatusColumn.ts | 96 +++++++++ .../SewerReservoirStatusColumn.ts | 101 ++++++++++ ...istics-sewage-reservoir-total-interface.ts | 114 +++++++++++ 4 files changed, 494 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts create mode 100644 apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts diff --git a/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx b/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx index 6016a7930..479e7b4b0 100644 --- a/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx +++ b/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx @@ -1,10 +1,190 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// 분리된 개별 컬럼 구성 임포트 경로 유지 +import { ReservoirStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn"; +import { PumpStationStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn"; + +// 수정 적용한 API 및 인터페이스 명세 호출 유지 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + SewerReservoirMaintainTotalRequest +} from "@/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface"; +import CellColorInfo from "./CellColorInfo"; + +const SewerReservoirColumnListTot = [ + ReservoirStatusColumnConfig, + PumpStationStatusColumnConfig +]; + +const firstTabKey = SewerReservoirColumnListTot[0]?.tabKey ?? "ST00040001"; const SewageReservoirPumpTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + + // 상태 정의에서 null을 제외하여 DataGrid와 타입을 일치시킵니다. + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = SewerReservoirColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + const param: SewerReservoirMaintainTotalRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + if (tabKey === "ST00040001") { + const response = await statisticsTotalStatusApi.getSewerReservoirStatusTotalList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + if (tabKey === "ST00040002") { + const response = await statisticsTotalStatusApi.getSewerPumpStationStatusTotalList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(activeTab, values); + 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); + if (existing && existing.length > 0) return; + + try { + setIsLoading(true); + await loadGridData(tabKey, values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + 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={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts b/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts new file mode 100644 index 000000000..64ce85216 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts @@ -0,0 +1,96 @@ +import { ValueType } from "realgrid"; + +export const PumpStationStatusColumnConfig = { + tabKey: "ST00040002", + tabLabel: "빗물펌프장", + rowStartNum: 2, + fixedColumns: 2, + 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 }, + + // 단일 컬럼들 + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "fcltyKndCn", fieldName: "fcltyKndCn", header: { text: "시설물종류" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planDesignFreqCnt", fieldName: "planDesignFreqCnt", header: { text: "계획설계빈도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "wtrStrgZoneArea", fieldName: "wtrStrgZoneArea", header: { text: "집수구역면적(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + + // 유수지 그룹 소속 + { name: "rsvrYn", fieldName: "rsvrYn", header: { text: "유수지유무\n(유,무)" }, editable: false }, + { name: "rsvrImpndQnt", fieldName: "rsvrImpndQnt", header: { text: "저류량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 흡수정 그룹 소속 + { name: "abstnRgnImdQnt", fieldName: "abstnRgnImdQnt", header: { text: "저류량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 단일 컬럼 + { name: "grssPpqty", fieldName: "grssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 펌프시설 > 펌프시설1 그룹 소속 + { name: "pumpFcltN1DmtrExpln", fieldName: "pumpFcltN1DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN1PerPpqty", fieldName: "pumpFcltN1PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN1PumpCnt", fieldName: "pumpFcltN1PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN1RsrvPumpCnt", fieldName: "pumpFcltN1RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설2 그룹 소속 + { name: "pumpFcltN2DmtrExpln", fieldName: "pumpFcltN2DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN2PerPpqty", fieldName: "pumpFcltN2PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN2PumpCnt", fieldName: "pumpFcltN2PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN2RsrvPumpCnt", fieldName: "pumpFcltN2RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설3 그룹 소속 + { name: "pumpFcltN3DmtrExpln", fieldName: "pumpFcltN3DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN3PerPpqty", fieldName: "pumpFcltN3PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN3PumpCnt", fieldName: "pumpFcltN3PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN3RsrvPumpCnt", fieldName: "pumpFcltN3RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 부대시설 그룹 소속 + { name: "prcsgFcltYn", fieldName: "prcsgFcltYn", header: { text: "전처리\n시설\n유무\n(유/무)" }, editable: false }, + { name: "ddrztnEqpmYn", fieldName: "ddrztnEqpmYn", header: { text: "탈취설비\n유무\n(유/무)" }, editable: false }, + { name: "oslfEtgtYn", fieldName: "oslfEtgtYn", header: { text: "자가발전\n유무\n(유/무)" }, editable: false }, + + // 단일 컬럼 + { name: "dschgWtrsExpln", fieldName: "dschgWtrsExpln", header: { text: "방류수역" }, editable: false }, + + // 최종처분량(ton/년) 그룹 소속 + { name: "prcptnLastDspsQnt", fieldName: "prcptnLastDspsQnt", header: { text: "침사" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ctmnLastDspsQnt", fieldName: "ctmnLastDspsQnt", header: { text: "협잡물" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 최종처분방법 그룹 소속 + { name: "prcptnLastDspsMthdExpln", fieldName: "prcptnLastDspsMthdExpln", header: { text: "침사" }, editable: false }, + { name: "ctmnLastDspsMthdExpln", fieldName: "ctmnLastDspsMthdExpln", header: { text: "협잡물" }, editable: false }, + + // 운영실적 그룹 소속 + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssMltvQnt", fieldName: "grssMltvQnt", header: { text: "총배수량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "fcltPstnCn", + "fcltyKndCn", + "oprtngStrtYmd", + "planDesignFreqCnt", + "wtrStrgZoneArea", + { name: "rsvrGroup", header: { text: "유수지" }, items: ["rsvrYn", "rsvrImpndQnt"] }, + { name: "abstnRgnGroup", header: { text: "흡수정" }, items: ["abstnRgnImdQnt"] }, + { + name: "pumpFcltyGroup", + header: { text: "펌프시설" }, + items: [ + "grssPpqty", + { name: "pump1SubGroup", header: { text: "펌프시설1" }, items: ["pumpFcltN1DmtrExpln", "pumpFcltN1PerPpqty", "pumpFcltN1PumpCnt", "pumpFcltN1RsrvPumpCnt"] }, + { name: "pump2SubGroup", header: { text: "펌프시설2" }, items: ["pumpFcltN2DmtrExpln", "pumpFcltN2PerPpqty", "pumpFcltN2PumpCnt", "pumpFcltN2RsrvPumpCnt"] }, + { name: "pump3SubGroup", header: { text: "펌프시설3" }, items: ["pumpFcltN3DmtrExpln", "pumpFcltN3PerPpqty", "pumpFcltN3PumpCnt", "pumpFcltN3RsrvPumpCnt"] } + ] + }, + { name: "subFcltGroup", header: { text: "부대시설" }, items: ["prcsgFcltYn", "ddrztnEqpmYn", "oslfEtgtYn"] }, + "dschgWtrsExpln", + { name: "lastDspsGroup", header: { text: "최종처분량(ton/년)" }, items: ["prcptnLastDspsQnt", "ctmnLastDspsQnt"] }, + { name: "lastMthdGroup", header: { text: "최종처분방법" }, items: ["prcptnLastDspsMthdExpln", "ctmnLastDspsMthdExpln"] }, + { name: "oprtngGroup", header: { text: "운영실적" }, items: ["oprtngDayCnt", "grssMltvQnt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts b/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts new file mode 100644 index 000000000..311cb6867 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts @@ -0,0 +1,101 @@ +import { ValueType } from "realgrid"; + +export const ReservoirStatusColumnConfig = { + tabKey: "ST00040001", + tabLabel: "하수저류시설", + rowStartNum: 2, + fixedColumns: 2, + 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 }, + + // 단일 컬럼들 + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planDesignFreqCnt", fieldName: "planDesignFreqCnt", header: { text: "계획설계빈도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 설치목적 그룹 소속 (O,X로 표기, 중복체크가능) + { name: "fldngPrvtnYn", fieldName: "fldngPrvtnYn", header: { text: "침수예방" }, editable: false }, + { name: "wqPlltnMtgtYn", fieldName: "wqPlltnMtgtYn", header: { text: "수질오염저감" }, editable: false }, + { name: "rtlztnUsgYn", fieldName: "rtlztnUsgYn", header: { text: "하수\n재이용\n용도" }, editable: false }, + + // 단일 컬럼들 + { name: "fcltStrctCn", fieldName: "fcltStrctCn", header: { text: "시설구조\n(일반지하식,\n지하터널식,\n기타)" }, editable: false }, + { name: "wtrStrgZoneArea", fieldName: "wtrStrgZoneArea", header: { text: "접수구역면적(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "impdFcltCpct", fieldName: "impdFcltCpct", header: { text: "시설용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "grssPpqty", fieldName: "grssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 펌프시설 > 펌프시설1 그룹 소속 + { name: "pumpFcltN1DmtrExpln", fieldName: "pumpFcltN1DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN1PerPpqty", fieldName: "pumpFcltN1PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN1PumpCnt", fieldName: "pumpFcltN1PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN1RsrvPumpCnt", fieldName: "pumpFcltN1RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설2 그룹 소속 + { name: "pumpFcltN2DmtrExpln", fieldName: "pumpFcltN2DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN2PerPpqty", fieldName: "pumpFcltN2PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN2PumpCnt", fieldName: "pumpFcltN2PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN2RsrvPumpCnt", fieldName: "pumpFcltN2RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설3 그룹 소속 + { name: "pumpFcltN3DmtrExpln", fieldName: "pumpFcltN3DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN3PerPpqty", fieldName: "pumpFcltN3PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN3PumpCnt", fieldName: "pumpFcltN3PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN3RsrvPumpCnt", fieldName: "pumpFcltN3RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 부대시설 그룹 소속 + { name: "prcsgFcltYn", fieldName: "prcsgFcltYn", header: { text: "전처리\n시설\n유무\n(유/무)" }, editable: false }, + { name: "ddrztnEqpmYn", fieldName: "ddrztnEqpmYn", header: { text: "탈취설비\n유무\n(유/무)" }, editable: false }, + { name: "oslfEtgtYn", fieldName: "oslfEtgtYn", header: { text: "자가발전\n유무\n(유/무)" }, editable: false }, + + // 단일 컬럼들 + { name: "mltvMthCn", fieldName: "mltvMthCn", header: { text: "배수방식\n(자연배수,\n강제배수)" }, editable: false }, + { name: "impdMthCn", fieldName: "impdMthCn", header: { text: "저류방식\n(직렬,병렬)" }, editable: false }, + { name: "impdPrcsMthdCn", fieldName: "impdPrcsMthdCn", header: { text: "저류수\n처리방법\n(자체처리,\n연계처리)" }, editable: false }, + { name: "dschgWtrsExpln", fieldName: "dschgWtrsExpln", header: { text: "방류수역\n(또는\n연계처리장명)" }, editable: false }, + + // 최종처분량(ton/년) 그룹 소속 + { name: "prcptnLastDspsQnt", fieldName: "prcptnLastDspsQnt", header: { text: "침사" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ctmnLastDspsQnt", fieldName: "ctmnLastDspsQnt", header: { text: "협잡물" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 최종처분방법 그룹 소속 + { name: "prcptnLastDspsMthdExpln", fieldName: "prcptnLastDspsMthdExpln", header: { text: "침사" }, editable: false }, + { name: "ctmnLastDspsMthdExpln", fieldName: "ctmnLastDspsMthdExpln", header: { text: "협잡물" }, editable: false }, + + // 운영실적 그룹 소속 + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssMltvQnt", fieldName: "grssMltvQnt", header: { text: "총배수량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "fcltPstnCn", + "oprtngStrtYmd", + "planDesignFreqCnt", + { name: "purposeGroup", header: { text: "설치목적(O,X로 표기, 중복체크가능)" }, items: ["fldngPrvtnYn", "wqPlltnMtgtYn", "rtlztnUsgYn"] }, + "fcltStrctCn", + "wtrStrgZoneArea", + "impdFcltCpct", + { + name: "pumpFcltyGroup", + header: { text: "펌프시설" }, + items: [ + "grssPpqty", + { name: "pump1SubGroup", header: { text: "펌프시설1" }, items: ["pumpFcltN1DmtrExpln", "pumpFcltN1PerPpqty", "pumpFcltN1PumpCnt", "pumpFcltN1RsrvPumpCnt"] }, + { name: "pump2SubGroup", header: { text: "펌프시설2" }, items: ["pumpFcltN2DmtrExpln", "pumpFcltN2PerPpqty", "pumpFcltN2PumpCnt", "pumpFcltN2RsrvPumpCnt"] }, + { name: "pump3SubGroup", header: { text: "펌프시설3" }, items: ["pumpFcltN3DmtrExpln", "pumpFcltN3PerPpqty", "pumpFcltN3PumpCnt", "pumpFcltN3RsrvPumpCnt"] } + ] + }, + { name: "subFcltGroup", header: { text: "부대시설" }, items: ["prcsgFcltYn", "ddrztnEqpmYn", "oslfEtgtYn"] }, + "mltvMthCn", + "impdMthCn", + "impdPrcsMthdCn", + "dschgWtrsExpln", + { name: "lastDspsGroup", header: { text: "최종처분량(ton/년)" }, items: ["prcptnLastDspsQnt", "ctmnLastDspsQnt"] }, + { name: "lastMthdGroup", header: { text: "최종처분방법" }, items: ["prcptnLastDspsMthdExpln", "ctmnLastDspsMthdExpln"] }, + { name: "oprtngGroup", header: { text: "운영실적" }, items: ["oprtngDayCnt", "grssMltvQnt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts new file mode 100644 index 000000000..782875e34 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts @@ -0,0 +1,114 @@ +export interface SewerReservoirMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] `GET .../reservoir-status` 목록 항목 */ +export interface SewerReservoirStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + sewerBzmnCd: string | null; + fcltNm: string; + fcltPstnCn: string; + rowType: string; + oprtngStrtYmd: string; + planDesignFreqCnt: number; + fldngPrvtnYn: string; + wqPlltnMtgtYn: string; + rtlztnUsgYn: string; + fcltStrctCn: string; + wtrStrgZoneArea: number; + impdFcltCpct: number; + grssPpqty: number; + pumpFcltN1DmtrExpln: string; + pumpFcltN1PerPpqty: number; + pumpFcltN1PumpCnt: number; + pumpFcltN1RsrvPumpCnt: number; + pumpFcltN2DmtrExpln: string; + pumpFcltN2PerPpqty: number; + pumpFcltN2PumpCnt: number; + pumpFcltN2RsrvPumpCnt: number; + pumpFcltN3DmtrExpln: string; + pumpFcltN3PerPpqty: number; + pumpFcltN3PumpCnt: number; + pumpFcltN3RsrvPumpCnt: number; + prcsgFcltYn: string; + ddrztnEqpmYn: string; + oslfEtgtYn: string; + mltvMthCn: string; + impdMthCn: string; + impdPrcsMthdCn: string; + dschgWtrsExpln: string; + prcptnLastDspsQnt: number; + ctmnLastDspsQnt: number; + prcptnLastDspsMthdExpln: string; + ctmnLastDspsMthdExpln: string; + oprtngDayCnt: number; + grssMltvQnt: number; + rmrk: string | null; +} + +/** [Tab1] `GET .../reservoir-status` 응답 */ +export interface SewerReservoirStatusTotalResponse { + status: string; + message?: string; + data: SewerReservoirStatusTotalItem[]; +} + +/** [Tab2] `GET .../pump-station-status` 목록 항목 */ +export interface SewerPumpStationStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + sewerBzmnCd: string | null; + rowType: string; + fcltNm: string; + fcltPstnCn: string; + fcltyKndCn: string; + oprtngStrtYmd: string; + planDesignFreqCnt: number; + wtrStrgZoneArea: number; + rsvrYn: string; + rsvrImpndQnt: number; + abstnRgnImdQnt: number; + grssPpqty: number; + pumpFcltN1DmtrExpln: string; + pumpFcltN1PerPpqty: number; + pumpFcltN1PumpCnt: number; + pumpFcltN1RsrvPumpCnt: number; + pumpFcltN2DmtrExpln: string; + pumpFcltN2PerPpqty: number; + pumpFcltN2PumpCnt: number; + pumpFcltN2RsrvPumpCnt: number; + pumpFcltN3DmtrExpln: string; + pumpFcltN3PerPpqty: number; + pumpFcltN3PumpCnt: number; + pumpFcltN3RsrvPumpCnt: number; + prcsgFcltYn: string; + ddrztnEqpmYn: string; + oslfEtgtYn: string; + dschgWtrsExpln: string; + prcptnLastDspsQnt: number; + ctmnLastDspsQnt: number; + prcptnLastDspsMthdExpln: string; + ctmnLastDspsMthdExpln: string; + oprtngDayCnt: number; + grssMltvQnt: number; +} + +/** [Tab2] `GET .../pump-station-status` 응답 */ +export interface SewerPumpStationStatusTotalResponse { + status: string; + message?: string; + data: SewerPumpStationStatusTotalItem[]; +} \ No newline at end of file From 85ba9fe0ab13873805b53f8956f02d5fe9c5b40a Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:48:45 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]05=20?= =?UTF-8?q?=ED=8E=8C=ED=94=84=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/total/PumpingStationTot.tsx | 119 +++++++++++++++++- .../total/SewerMediationPumpStatusColumn.ts | 103 +++++++++++++++ ...atistics-mediation-pump-total-interface.ts | 67 ++++++++++ 3 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts diff --git a/apps/st/src/components/statistics/total/PumpingStationTot.tsx b/apps/st/src/components/statistics/total/PumpingStationTot.tsx index fd4cc679d..b3da0a044 100644 --- a/apps/st/src/components/statistics/total/PumpingStationTot.tsx +++ b/apps/st/src/components/statistics/total/PumpingStationTot.tsx @@ -1,10 +1,123 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { LoadingWithAnimation } 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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// 분리된 중계펌프장용 컬럼 구성 임포트 +import { MediationPumpStatusColumnConfig } from "@/components/util/column/total/SewerMediationPumpStatusColumn"; + +// 집계표(TOTAL) API 및 인터페이스 명세 호출 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + MediationPumpMaintainTotalRequest +} from "@/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface"; +import CellColorInfo from "./CellColorInfo"; const PumpingStationTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + + // null 타입 컴파일 에러가 발생하지 않도록 엄격하게 정의한 데이터 상태 + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const lastSearchedValuesRef = useRef(null); + + const config = MediationPumpStatusColumnConfig; + + const handleReset = () => { + setRowData([]); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (values: SearchFormValues): Promise => { + const param: MediationPumpMaintainTotalRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + const response = await statisticsTotalStatusApi.getMediationPumpStationStatusTotalList(param); + if (response && response.status === "success" && response.data) { + // object[]로 변환하여 주입함으로써 null 에러 전면 방지 + setRowData(toGridRows(response.data as object[])); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(values); + if (ok) { + lastSearchedValuesRef.current = values; + } else { + lastSearchedValuesRef.current = null; + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; + } finally { + setIsLoading(false); + } + }; + + const gridColumnInfo: ColumnInfo = { + columns: config.columns, + columnLayout: config.columnLayout, + }; + 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/util/column/total/SewerMediationPumpStatusColumn.ts b/apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts new file mode 100644 index 000000000..66451c04b --- /dev/null +++ b/apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts @@ -0,0 +1,103 @@ +import { ValueType } from "realgrid"; + +export const MediationPumpStatusColumnConfig = { + tabKey: "ST00050001", + tabLabel: "중계펌프장", + rowStartNum: 2, + fixedColumns: 3, + 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 }, + + // 단일 컬럼들 + { name: "fcltNm", fieldName: "fcltNm", header: { text: "펌프장명" }, editable: false }, + { name: "pumpSeNm", fieldName: "pumpSeNm", header: { text: "구분\n(합류식,\n분류식)" }, editable: false }, + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planMltvArea", fieldName: "planMltvArea", header: { text: "계획배수면적(㏊)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 계획시간대최대배수량(㎥/일) 그룹 소속 + { name: "sndyArea", fieldName: "sndyArea", header: { text: "청천시" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rainArea", fieldName: "rainArea", header: { text: "우천시" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnQnt", fieldName: "exlnQnt", header: { text: "우수량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수펌프 그룹 내부 소속 + { name: "drtwPumpGrssPpqty", fieldName: "drtwPumpGrssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수펌프 > 오수펌프1 그룹 소속 + { name: "drtwPumpN1DmtrArea", fieldName: "drtwPumpN1DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN1Ppqty", fieldName: "drtwPumpN1Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN1Cnt", fieldName: "drtwPumpN1Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN1RsrvCnt", fieldName: "drtwPumpN1RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 오수펌프 > 오수펌프2 그룹 소속 + { name: "drtwPumpN2DmtrArea", fieldName: "drtwPumpN2DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN2Ppqty", fieldName: "drtwPumpN2Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN2Cnt", fieldName: "drtwPumpN2Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN2RsrvCnt", fieldName: "drtwPumpN2RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 오수펌프 > 오수펌프3 그룹 소속 + { name: "drtwPumpN3DmtrArea", fieldName: "drtwPumpN3DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN3Ppqty", fieldName: "drtwPumpN3Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN3Cnt", fieldName: "drtwPumpN3Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN3RsrvCnt", fieldName: "drtwPumpN3RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 그룹 내부 소속 + { name: "exlnPumpGrssPpqty", fieldName: "exlnPumpGrssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 우수펌프 > 우수펌프1 그룹 소속 + { name: "exlnPumpN1DmtrArea", fieldName: "exlnPumpN1DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN1Ppqty", fieldName: "exlnPumpN1Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN1Cnt", fieldName: "exlnPumpN1Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN1RsrvCnt", fieldName: "exlnPumpN1RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 > 우수펌프2 그룹 소속 + { name: "exlnPumpN2DmtrArea", fieldName: "exlnPumpN2DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN2Ppqty", fieldName: "exlnPumpN2Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN2Cnt", fieldName: "exlnPumpN2Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN2RsrvCnt", fieldName: "exlnPumpN2RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 > 우수펌프3 그룹 소속 + { name: "exlnPumpN3DmtrArea", fieldName: "exlnPumpN3DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN3Ppqty", fieldName: "exlnPumpN3Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN3Cnt", fieldName: "exlnPumpN3Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN3RsrvCnt", fieldName: "exlnPumpN3RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 단일 컬럼 + { name: "anlElpwrUseQnt", fieldName: "anlElpwrUseQnt", header: { text: "연간전력\n사용량\n(㎾h/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtFcltNm", + "fcltNm", + "pumpSeNm", + "fcltPstnCn", + "oprtngStrtYmd", + "planMltvArea", + { name: "planDrainGroup", header: { text: "계획시간대최대배수량(㎥/일)" }, items: ["sndyArea", "rainArea", "exlnQnt"] }, + { + name: "drtwPumpGroup", + header: { text: "오수펌프" }, + items: [ + "drtwPumpGrssPpqty", + { name: "drtwPumpN1Group", header: { text: "오수펌프(1)" }, items: ["drtwPumpN1DmtrArea", "drtwPumpN1Ppqty", "drtwPumpN1Cnt", "drtwPumpN1RsrvCnt"] }, + { name: "drtwPumpN2Group", header: { text: "오수펌프(2)" }, items: ["drtwPumpN2DmtrArea", "drtwPumpN2Ppqty", "drtwPumpN2Cnt", "drtwPumpN2RsrvCnt"] }, + { name: "drtwPumpN3Group", header: { text: "오수펌프(3)" }, items: ["drtwPumpN3DmtrArea", "drtwPumpN3Ppqty", "drtwPumpN3Cnt", "drtwPumpN3RsrvCnt"] } + ] + }, + { + name: "exlnPumpGroup", + header: { text: "우수펌프" }, + items: [ + "exlnPumpGrssPpqty", + { name: "exlnPumpN1Group", header: { text: "우수펌프(1)" }, items: ["exlnPumpN1DmtrArea", "exlnPumpN1Ppqty", "exlnPumpN1Cnt", "exlnPumpN1RsrvCnt"] }, + { name: "exlnPumpN2Group", header: { text: "우수펌프(2)" }, items: ["exlnPumpN2DmtrArea", "exlnPumpN2Ppqty", "exlnPumpN2Cnt", "exlnPumpN2RsrvCnt"] }, + { name: "exlnPumpN3Group", header: { text: "우수펌프(3)" }, items: ["exlnPumpN3DmtrArea", "exlnPumpN3Ppqty", "exlnPumpN3Cnt", "exlnPumpN3RsrvCnt"] } + ] + }, + "anlElpwrUseQnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts new file mode 100644 index 000000000..32c6cb5d3 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts @@ -0,0 +1,67 @@ +/** 05. 중계펌프장 현황 검색 조건 파라미터 */ +export interface MediationPumpMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + pumpstNm?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** 중계펌프장 현황 목록 데이터 단위 */ +export interface MediationPumpStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + pumpSeNm: string; + swtrmtFcltNm: string; + fcltPstnCn: string; + oprtngStrtYmd: string; + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + // 오수펌프 + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; + drtwPumpN2DmtrArea: number; + drtwPumpN2Ppqty: number; + drtwPumpN2Cnt: number; + drtwPumpN2RsrvCnt: number; + drtwPumpN3DmtrArea: number; + drtwPumpN3Ppqty: number; + drtwPumpN3Cnt: number; + drtwPumpN3RsrvCnt: number; + // 우수펌프 + exlnPumpGrssPpqty: number; + exlnPumpN1DmtrArea: number; + exlnPumpN1Ppqty: number; + exlnPumpN1Cnt: number; + exlnPumpN1RsrvCnt: number; + exlnPumpN2DmtrArea: number; + exlnPumpN2Ppqty: number; + exlnPumpN2Cnt: number; + exlnPumpN2RsrvCnt: number; + exlnPumpN3DmtrArea: number; + exlnPumpN3Ppqty: number; + exlnPumpN3Cnt: number; + exlnPumpN3RsrvCnt: number; + // 전력 + anlElpwrUseQnt: number; +} + +/** 중계펌프장 API 최종 응답 규격 */ +export interface MediationPumpStatusTotalResponse { + status: string; + message?: string; + data: MediationPumpStatusTotalItem[]; +} \ No newline at end of file From 3c701359fde88e3737506e618e13434b4ccb6fd5 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:50:54 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]?= =?UTF-8?q?=EB=B6=84=EB=87=A8=EC=B2=98=EB=A6=AC=EC=8B=9C=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/ExcretaTreatmentFacilityTot.tsx | 119 +++++++++++++++++- .../SewerExcretaTreatmentStatusColumn.ts | 63 ++++++++++ ...stics-excreta-treatment-total-interface.ts | 62 +++++++++ 3 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts diff --git a/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx b/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx index e470b07fa..a5c7f2dae 100644 --- a/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx +++ b/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx @@ -1,10 +1,123 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { LoadingWithAnimation } 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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// 분리된 분뇨처리시설용 컬럼 구성 임포트 +import { ExcretaTreatmentStatusColumnConfig } from "@/components/util/column/total/SewerExcretaTreatmentStatusColumn"; + +// 집계표(TOTAL) API 및 인터페이스 명세 호출 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + ExcretaTreatmentMaintainTotalRequest +} from "@/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface"; +import CellColorInfo from "./CellColorInfo"; const ExcretaTreatmentFacilityTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + + // null 타입 컴파일 에러를 방지하기 위해 엄격하게 정의한 데이터 상태 + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const lastSearchedValuesRef = useRef(null); + + const config = ExcretaTreatmentStatusColumnConfig; + + const handleReset = () => { + setRowData([]); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (values: SearchFormValues): Promise => { + const param: ExcretaTreatmentMaintainTotalRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + const response = await statisticsTotalStatusApi.getExcretaTreatmentStatusTotalList(param); + if (response && response.status === "success" && response.data) { + // object[]로 변환하여 주입함으로써 상위 컴포넌트 간 null 에러 완벽 차단 + setRowData(toGridRows(response.data as object[])); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(values); + if (ok) { + lastSearchedValuesRef.current = values; + } else { + lastSearchedValuesRef.current = null; + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; + } finally { + setIsLoading(false); + } + }; + + const gridColumnInfo: ColumnInfo = { + columns: config.columns, + columnLayout: config.columnLayout, + }; + 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/util/column/total/SewerExcretaTreatmentStatusColumn.ts b/apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts new file mode 100644 index 000000000..0f5ded556 --- /dev/null +++ b/apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts @@ -0,0 +1,63 @@ +import { ValueType } from "realgrid"; + +export const ExcretaTreatmentStatusColumnConfig = { + tabKey: "ST00070001", + tabLabel: "분뇨처리시설", + rowStartNum: 2, + fixedColumns: 2, + 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 }, + + // 기본 마스터 상세 + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "소재지" }, editable: false }, + { name: "telNo", fieldName: "telNo", header: { text: "전화번호" }, editable: false }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "준공일" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일" }, editable: false }, + { name: "wholPjtcoAmt", fieldName: "wholPjtcoAmt", header: { text: "사업비\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsFcltCpct", fieldName: "prcsFcltCpct", header: { text: "시설용량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "cpct", fieldName: "cpct", header: { text: "처리량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsMthdExpln", fieldName: "prcsMthdExpln", header: { text: "처리공법" }, editable: false }, + { name: "linkTmplntCn", fieldName: "linkTmplntCn", header: { text: "연계처리장명" }, editable: false }, + { name: "linkPrcsQnt", fieldName: "linkPrcsQnt", header: { text: "연계처리량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 유입수질(mg/L, 개/mL) 그룹 소속 + { name: "inflwBodWqVl", fieldName: "inflwBodWqVl", header: { text: "BOD" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTocWqVl", fieldName: "inflwTocWqVl", header: { text: "TOC" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwSsWqVl", fieldName: "inflwSsWqVl", header: { text: "SS" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTnWqVl", fieldName: "inflwTnWqVl", header: { text: "T-N" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTpWqVl", fieldName: "inflwTpWqVl", header: { text: "T-P" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.000", editable: false }, + { name: "inflwClfgCnt", fieldName: "inflwClfgCnt", header: { text: "대장균\n군수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 방류수질(mg/L, 개/mL) 그룹 소속 + { name: "dschgBodWqVl", fieldName: "dschgBodWqVl", header: { text: "BOD" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTocWqVl", fieldName: "dschgTocWqVl", header: { text: "TOC" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgSsWqVl", fieldName: "dschgSsWqVl", header: { text: "SS" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTnWqVl", fieldName: "dschgTnWqVl", header: { text: "T-N" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTpWqVl", fieldName: "dschgTpWqVl", header: { text: "T-P" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.000", editable: false }, + { name: "dschgClfgCnt", fieldName: "dschgClfgCnt", header: { text: "대장균\n군수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 방류수역 그룹 소속 + { name: "wtsysNm", fieldName: "wtsysNm", header: { text: "수계" }, editable: false }, + { name: "trbtrNm", fieldName: "trbtrNm", header: { text: "지류" }, editable: false }, + { name: "msirbCn", fieldName: "msirbCn", header: { text: "중권역" }, editable: false }, + { name: "ssirbCn", fieldName: "ssirbCn", header: { text: "소권역" }, editable: false }, + + // 단일 미그룹 성격 컬럼들 + { name: "operMthdCdCn", fieldName: "operMthdCdCn", header: { text: "운영주체\n(자체/공기업/\n민간위탁)" }, editable: false }, + { name: "wholNope", fieldName: "wholNope", header: { text: "직원총수\n(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sftyAcdntEvntcsCnt", fieldName: "sftyAcdntEvntcsCnt", header: { text: "안전사고\n건수\n(건/인/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", "fcltPstnCn", "telNo", "cmptnStrtYmd", "oprtngStrtYmd", "wholPjtcoAmt", "prcsFcltCpct", "cpct", "oprtngDayCnt", "prcsMthdExpln", "linkTmplntCn", "linkPrcsQnt", + { name: "inflwWqGroup", header: { text: "유입수질(mg/L, 개/mL)" }, items: ["inflwBodWqVl", "inflwTocWqVl", "inflwSsWqVl", "inflwTnWqVl", "inflwTpWqVl", "inflwClfgCnt"] }, + { name: "dschgWqGroup", header: { text: "방류수질(mg/L, 개/mL)" }, items: ["dschgBodWqVl", "dschgTocWqVl", "dschgSsWqVl", "dschgTnWqVl", "dschgTpWqVl", "dschgClfgCnt"] }, + { name: "wtrAreaGroup", header: { text: "방류수역" }, items: ["wtsysNm", "trbtrNm", "msirbCn", "ssirbCn"] }, + "operMthdCdCn", "wholNope", "sftyAcdntEvntcsCnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts new file mode 100644 index 000000000..dc9446b32 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts @@ -0,0 +1,62 @@ +/** 07. 분뇨처리시설 현황 검색 조건 파라미터 DTO */ +export interface ExcretaTreatmentMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** 분뇨처리시설 목록 데이터 행 구조 */ +export interface ExcretaTreatmentStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + fcltNm: string; + rowType: string; + fcltPstnCn: string; + telNo: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string; + linkTmplntCn: string; + linkPrcsQnt: number; + // 유입수질 + inflwBodWqVl: number; + inflwTocWqVl: number; + inflwSsWqVl: number; + inflwTnWqVl: number; + inflwTpWqVl: number; + inflwClfgCnt: number; + // 방류수질 + dschgBodWqVl: number; + dschgTocWqVl: number; + dschgSsWqVl: number; + dschgTnWqVl: number; + dschgTpWqVl: number; + dschgClfgCnt: number; + // 방류수역 및 기타 정보 + wtsysNm: string; + trbtrNm: string; + msirbCn: string; + ssirbCn: string; + operMthdCdCn: string; + wholNope: number; + sftyAcdntEvntcsCnt: number; +} + +/** 분뇨처리시설 API 응답 인터페이스 */ +export interface ExcretaTreatmentStatusTotalResponse { + status: string; + message?: string; + data: ExcretaTreatmentStatusTotalItem[]; +} \ No newline at end of file From a11eaf031d9ba479ab0dba57772dc83e9a16948c Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:52:54 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]08.?= =?UTF-8?q?=EA=B0=9C=EC=9D=B8=ED=95=98=EC=88=98=EC=B2=98=EB=A6=AC=EC=8B=9C?= =?UTF-8?q?=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/PrivateSewageFacilityTot.tsx | 202 +++++++++++++++++- .../SewerPrivateSewageTab1Column.ts | 46 ++++ .../SewerPrivateSewageTab2Column.ts | 72 +++++++ .../SewerPrivateSewageTab3Column.ts | 48 +++++ ...atistics-private-sewage-total-interface.ts | 109 ++++++++++ 5 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts diff --git a/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx b/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx index 1171f7b2c..0948ab60e 100644 --- a/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx +++ b/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx @@ -1,10 +1,206 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// 분리된 개별 3개 컬럼 스펙 파일 로드 +import { PrivateSewageTab1ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column"; +import { PrivateSewageTab2ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column"; +import { PrivateSewageTab3ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column"; + +// 통계 API 서비스 및 통합 DTO 명세 임포트 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + PrivateSewageFacilitySummaryRequest +} from "@/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface"; +import CellColorInfo from "./CellColorInfo"; + +const PrivateSewageColumnListTot = [ + PrivateSewageTab1ColumnConfig, + PrivateSewageTab2ColumnConfig, + PrivateSewageTab3ColumnConfig +]; + +const firstTabKey = PrivateSewageColumnListTot[0]?.tabKey ?? "ST00040005_1"; const PrivateSewageFacilityTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + + // null 컴파일 예러를 사전에 원천 차단한 안전 상태 맵 선언 + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = PrivateSewageColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + /** 각 탭별 엔드포인트 비동기 디스패치 및 연동 */ + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + const param: PrivateSewageFacilitySummaryRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + // [Tab1] 처리용량별 오수처리시설 + if (tabKey === "ST00080001") { + const response = await statisticsTotalStatusApi.getPrivateSewageTab1List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [Tab2] 정화조현황 + if (tabKey === "ST00080002") { + const response = await statisticsTotalStatusApi.getPrivateSewageTab2List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [Tab3] 개인하수도 관리지역 지정공고 현황 + if (tabKey === "ST00080003") { + const response = await statisticsTotalStatusApi.getPrivatePrivateMgmtAreaList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(activeTab, values); + 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); + if (existing && existing.length > 0) return; // 캐싱 검증 스킵 + + try { + setIsLoading(true); + await loadGridData(tabKey, values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {/* 하수관로 규칙과 100% 싱크 맞춘 중앙 정렬 소형 팝업 로딩 바 고정 */} + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + + // 단언 연산자(as) 배제 형태 완벽 적용 + 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={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts new file mode 100644 index 000000000..c4af5d5a6 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts @@ -0,0 +1,46 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab1ColumnConfig = { + tabKey: "ST00080001", + tabLabel: "처리용량별 오수처리시설", + rowStartNum: 3, + fixedColumns: 3, + 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 }, + { name: "wholDrtwPrcsFcltCnt", fieldName: "wholDrtwPrcsFcltCnt", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 내 + { name: "wholStaaCnt", fieldName: "wholStaaCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaPrcsQnt", fieldName: "step1StaaPrcsQnt", header: { text: "2이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaPrcsQnt", fieldName: "step2StaaPrcsQnt", header: { text: "3~10" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaPrcsQnt", fieldName: "step3StaaPrcsQnt", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaPrcsQnt", fieldName: "step4StaaPrcsQnt", header: { text: "21~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaPrcsQnt", fieldName: "step5StaaPrcsQnt", header: { text: "51~100" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaPrcsQnt", fieldName: "step6StaaPrcsQnt", header: { text: "101~500" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaPrcsQnt", fieldName: "step7StaaPrcsQnt", header: { text: "501이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 외 + { name: "staaExcpWhol", fieldName: "staaExcpWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpPrcsQnt", fieldName: "step1StaaExcpPrcsQnt", header: { text: "2이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaExcpPrcsQnt", fieldName: "step2StaaExcpPrcsQnt", header: { text: "3~10" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaExcpPrcsQnt", fieldName: "step3StaaExcpPrcsQnt", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaExcpPrcsQnt", fieldName: "step4StaaExcpPrcsQnt", header: { text: "21~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaExcpPrcsQnt", fieldName: "step5StaaExcpPrcsQnt", header: { text: "51~100" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaExcpPrcsQnt", fieldName: "step6StaaExcpPrcsQnt", header: { text: "101~500" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaExcpPrcsQnt", fieldName: "step7StaaExcpPrcsQnt", header: { text: "501이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "wholDrtwPrcsFcltCnt", + { + name: "privateSewageFacilityGroup", + header: { text: "오수처리시설" }, + items: [ + { name: "insideGroup", header: { text: "하수처리구역 내(㎥/일)" }, items: ["wholStaaCnt", "step1StaaPrcsQnt", "step2StaaPrcsQnt", "step3StaaPrcsQnt", "step4StaaPrcsQnt", "step5StaaPrcsQnt", "step6StaaPrcsQnt", "step7StaaPrcsQnt"] }, + { name: "outsideGroup", header: { text: "하수처리구역 외(㎥/일)" }, items: ["staaExcpWhol", "step1StaaExcpPrcsQnt", "step2StaaExcpPrcsQnt", "step3StaaExcpPrcsQnt", "step4StaaExcpPrcsQnt", "step5StaaExcpPrcsQnt", "step6StaaExcpPrcsQnt", "step7StaaExcpPrcsQnt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts new file mode 100644 index 000000000..19fc4d1fb --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts @@ -0,0 +1,72 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab2ColumnConfig = { + tabKey: "ST00080002", + tabLabel: "정화조현황", + rowStartNum: 4, + fixedColumns: 3, + 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 }, + { name: "grssFcltCnt", fieldName: "grssFcltCnt", header: { text: "총 시설 수\n(A+D)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 내 소속 항목들 + { name: "staaInsdWhol", fieldName: "staaInsdWhol", header: { text: "총계\nA=(B+C)/2" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdPrcsCpctByWhol", fieldName: "staaInsdPrcsCpctByWhol", header: { text: "계(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdSttkNprsflCnt", fieldName: "staaInsdSttkNprsflCnt", header: { text: "자연유하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaInsdSttkFrcCnt", fieldName: "step1StaaInsdSttkFrcCnt", header: { text: "강제펌핑식" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdSprktpFltrtbCnt", fieldName: "staaInsdSprktpFltrtbCnt", header: { text: "살수여상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdAertMthdCnt", fieldName: "staaInsdAertMthdCnt", header: { text: "폭기방법" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdCntcAertCnt", fieldName: "staaInsdCntcAertCnt", header: { text: "접촉폭기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdEtcCnt", fieldName: "staaInsdEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaInsdNope", fieldName: "step1StaaInsdNope", header: { text: "10인조\n이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaInsdNope", fieldName: "step2StaaInsdNope", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaInsdNope", fieldName: "step3StaaInsdNope", header: { text: "21~30" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaInsdNope", fieldName: "step4StaaInsdNope", header: { text: "31~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaInsdNope", fieldName: "step5StaaInsdNope", header: { text: "51~199" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaInsdNope", fieldName: "step6StaaInsdNope", header: { text: "200~499" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaInsdNope", fieldName: "step7StaaInsdNope", header: { text: "500~999" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step8StaaInsdNope", fieldName: "step8StaaInsdNope", header: { text: "1,000이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 외 소속 항목들 + { name: "staaExcpWhol", fieldName: "staaExcpWhol", header: { text: "총계\nD=(E+F)/2" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpPrcsCpctByWhol", fieldName: "staaExcpPrcsCpctByWhol", header: { text: "계(F)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpSttkNprsflCnt", fieldName: "staaExcpSttkNprsflCnt", header: { text: "자연유하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpSttkFrcCnt", fieldName: "step1StaaExcpSttkFrcCnt", header: { text: "강제펌핑식" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpSprktpFltrtbCnt", fieldName: "staaExcpSprktpFltrtbCnt", header: { text: "살수여상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpAertMthdCnt", fieldName: "staaExcpAertMthdCnt", header: { text: "폭기방법" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpCntcAertCnt", fieldName: "staaExcpCntcAertCnt", header: { text: "접촉폭기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpEtcCnt", fieldName: "staaExcpEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpNope", fieldName: "step1StaaExcpNope", header: { text: "10인조\n이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaExcpNope", fieldName: "step2StaaExcpNope", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaExcpNope", fieldName: "step3StaaExcpNope", header: { text: "21~30" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaExcpNope", fieldName: "step4StaaExcpNope", header: { text: "31~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaExcpNope", fieldName: "step5StaaExcpNope", header: { text: "51~199" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaExcpNope", fieldName: "step6StaaExcpNope", header: { text: "200~499" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaExcpNope", fieldName: "step7StaaExcpNope", header: { text: "500~999" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step8StaaExcpNope", fieldName: "step8StaaExcpNope", header: { text: "1,000이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "grssFcltCnt", + { + name: "insideSewerGroup", + header: { text: "하수처리구역 내" }, + items: [ + "staaInsdWhol", + { name: "insdBGroup", header: { text: "계(B)" }, items: ["staaInsdSttkNprsflCnt", "step1StaaInsdSttkFrcCnt", "staaInsdSprktpFltrtbCnt", "staaInsdAertMthdCnt", "staaInsdCntcAertCnt", "staaInsdEtcCnt"] }, + { name: "insdCGroup", header: { text: "처리용량별" }, items: ["staaInsdPrcsCpctByWhol", "step1StaaInsdNope", "step2StaaInsdNope", "step3StaaInsdNope", "step4StaaInsdNope", "step5StaaInsdNope", "step6StaaInsdNope", "step7StaaInsdNope", "step8StaaInsdNope"] } + ] + }, + { + name: "outsideSewerGroup", + header: { text: "하수처리구역 외" }, + items: [ + "staaExcpWhol", + { name: "excpEGroup", header: { text: "계(E)" }, items: ["staaExcpSttkNprsflCnt", "step1StaaExcpSttkFrcCnt", "staaExcpSprktpFltrtbCnt", "staaExcpAertMthdCnt", "staaExcpCntcAertCnt", "staaExcpEtcCnt"] }, + { name: "excpFGroup", header: { text: "처리용량별" }, items: ["staaExcpPrcsCpctByWhol", "step1StaaExcpNope", "step2StaaExcpNope", "step3StaaExcpNope", "step4StaaExcpNope", "step5StaaExcpNope", "step6StaaExcpNope", "step7StaaExcpNope", "step8StaaExcpNope"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts new file mode 100644 index 000000000..98a629759 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts @@ -0,0 +1,48 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab3ColumnConfig = { + tabKey: "ST00080003", // 08번 개인하수처리시설 TAB3 매핑 키 고정 + tabLabel: "개인하수도 관리지역 지정공고 현황", + rowStartNum: 3, // 3단 다단 레이아웃 스펙에 맞추어 3으로 설정합니다. + fixedColumns: 1, + 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 }, + + // 마스터 단일 컬럼 영역 (개소, 면적) + { name: "indvSwagePbancRgnCnt", fieldName: "indvSwagePbancRgnCnt", header: { text: "공고지역\n(개소)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "indvSwageDsgnScpLen", fieldName: "indvSwageDsgnScpLen", header: { text: "지정범위\n(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + + // 정화조 그룹 소속 (개소수, 처리용량) + { name: "spctnkGrssFcltCnt", fieldName: "spctnkGrssFcltCnt", header: { text: "총시설수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "spctnkQnt", fieldName: "spctnkQnt", header: { text: "처리용량\n(인/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수처리시설 그룹 소속 (개소수, 처리용량) + { name: "drtwPrcsFcltGrssFcltCnt", fieldName: "drtwPrcsFcltGrssFcltCnt", header: { text: "총시설수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPrcsFcltQnt", fieldName: "drtwPrcsFcltQnt", header: { text: "처리용량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + // 행정구역 묶음 + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + + // 메인 마스터 지표 + "indvSwagePbancRgnCnt", + "indvSwageDsgnScpLen", + + // 정화조 다단 묶음 + { + name: "septicTankGroup", + header: { text: "정화조" }, + items: ["spctnkGrssFcltCnt", "spctnkQnt"] + }, + + // 오수처리시설 다단 묶음 + { + name: "sewageTreatmentGroup", + header: { text: "오수처리시설" }, + items: ["drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltQnt"] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts new file mode 100644 index 000000000..43c4f4c9a --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts @@ -0,0 +1,109 @@ +/** 08. 개인하수처리시설 공통 요청 검색 파라미터 */ +export interface PrivateSewageFacilitySummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; // Controller 수신 규격 + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 오수처리시설 목록 항목 */ +export interface PrivateSewageTab1StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + wholDrtwPrcsFcltCnt: number; + // 하수처리구역 내 + wholStaaCnt: number; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + // 하수처리구역 외 + staaExcpWhol: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; +} + +/** [Tab2] 정화조 현황 목록 항목 */ +export interface PrivateSewageTab2StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + grssFcltCnt: number; + // 하수처리구역 내 + staaInsdWhol: number; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + staaInsdPrcsCpctByWhol: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + // 하수처리구역 외 + staaExcpWhol: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + staaExcpPrcsCpctByWhol: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; +} + +/** [Tab3] 개인하수도 관리지역 지정공고 현황 목록 항목 */ +export interface PrivateSewageTab3StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; +} + +/** 3개 탭 개별 API 응답 규격 정의 */ +export interface PrivateSewageTab1Response { status: string; message?: string; data: PrivateSewageTab1StatusItem[]; } +export interface PrivateSewageTab2Response { status: string; message?: string; data: PrivateSewageTab2StatusItem[]; } +export interface PrivateSewageTab3Response { status: string; message?: string; data: PrivateSewageTab3StatusItem[]; } \ No newline at end of file From 6cda4139131707a62ae90a5596259cfa876d9269 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:55:06 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]09.?= =?UTF-8?q?=ED=95=98=EC=88=98=EB=8F=84=20=EC=9E=AC=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/total/SewerageFinanceTot.tsx | 213 +++++++++++++++++- .../SewerageFeeTab2Column.ts | 24 ++ .../SewerageFinancialTab1Column.ts | 58 +++++ .../SewerageOrgCauseTab4Column.ts | 38 ++++ .../SewerageUsageFeeTab3Column.ts | 62 +++++ ...istics-sewerage-finance-total-interface.ts | 121 ++++++++++ 6 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts diff --git a/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx b/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx index a73eaba2f..7749bac68 100644 --- a/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx +++ b/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx @@ -1,10 +1,217 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// 분리된 4개 개별 탭 컬럼 파일 바인딩 +import { SewerageFinancialTab1ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column"; +import { SewerageFeeTab2ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column"; +import { SewerageUsageFeeTab3ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column"; +import { SewerageOrgCauseTab4ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column"; + +// API 인스턴스 및 인터페이스 호출 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + SewerageFinanceSummaryRequest +} from "@/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface"; +import CellColorInfo from "./CellColorInfo"; + +const SewerageFinanceColumnListTot = [ + SewerageFinancialTab1ColumnConfig, + SewerageFeeTab2ColumnConfig, + SewerageUsageFeeTab3ColumnConfig, + SewerageOrgCauseTab4ColumnConfig +]; + +const firstTabKey = SewerageFinanceColumnListTot[0]?.tabKey ?? "ST00040006_1"; const SewerageFinanceTot = () => { + 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 = SewerageFinanceColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + /** 개별 조건별 데이터 동적 라우팅 디스패치 */ + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + const param: SewerageFinanceSummaryRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + // [Tab1] 하수도재정 결산 + if (tabKey === "ST00090001") { + const response = await statisticsTotalStatusApi.getSewerageFinancialList(param); // 엔드포인트 바인딩 + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [Tab2] 하수도 요금 + if (tabKey === "ST00090002") { + const response = await statisticsTotalStatusApi.getSewerageFeeList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [Tab3] 하수도 사용료 + if (tabKey === "ST00090003") { + const response = await statisticsTotalStatusApi.getSewerageUsageList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [Tab4] 원인자 부담금 + if (tabKey === "ST00090004") { + const response = await statisticsTotalStatusApi.getSewerageOrgCauseList(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(activeTab, values); + 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); + if (existing && existing.length > 0) return; + + try { + setIsLoading(true); + await loadGridData(tabKey, values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + + // 요청하신 타입 단언 제거 형태 유지 + 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={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts new file mode 100644 index 000000000..2424c4ce7 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts @@ -0,0 +1,24 @@ +import { ValueType } from "realgrid"; + +export const SewerageFeeTab2ColumnConfig = { + tabKey: "ST00090002", + tabLabel: "하수도 요금", + rowStartNum: 2, + fixedColumns: 3, + 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 }, + + { name: "anlLevyQnt", fieldName: "anlLevyQnt", header: { text: "연간조정량(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "levyAmt", fieldName: "levyAmt", header: { text: "하수도사용료수익(원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "avgUntprcAmt", fieldName: "avgUntprcAmt", header: { text: "㎡당 요금(원/㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "grssPrmcoAmt", fieldName: "grssPrmcoAmt", header: { text: "총관원가(원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssUnitPrmcoAmt", fieldName: "grssUnitPrmcoAmt", header: { text: "㎡당원가(원/㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rlztnRt", fieldName: "rlztnRt", header: { text: "현실화율(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "anlLevyQnt", "levyAmt", "avgUntprcAmt", "grssPrmcoAmt", "grssUnitPrmcoAmt", "rlztnRt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts new file mode 100644 index 000000000..78589a22d --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts @@ -0,0 +1,58 @@ +import { ValueType } from "realgrid"; + +export const SewerageFinancialTab1ColumnConfig = { + tabKey: "ST00090001", + tabLabel: "하수도재정 결산", + rowStartNum: 3, + fixedColumns: 1, + 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 }, + + { name: "crfwAmt", fieldName: "crfwAmt", header: { text: "이월액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "lbltAmt", fieldName: "lbltAmt", header: { text: "부채액\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "usgByWholPjtcoAmt", fieldName: "usgByWholPjtcoAmt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "tmplntWholAmt", fieldName: "tmplntWholAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntFcltAmt", fieldName: "tmplntFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntMendngAmt", fieldName: "tmplntMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntOperKeepAmt", fieldName: "tmplntOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntEtcAmt", fieldName: "tmplntEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "wholSwagePplAmt", fieldName: "wholSwagePplAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplFcltAmt", fieldName: "swagePplFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplMendngAmt", fieldName: "swagePplMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplOperKeepAmt", fieldName: "swagePplOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplEtcAmt", fieldName: "swagePplEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "wholSwageImdpFcltAmt", fieldName: "wholSwageImdpFcltAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdFcltAmt", fieldName: "swageImpdFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdMendngAmt", fieldName: "swageImpdMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdOperKeepAmt", fieldName: "swageImpdOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdFcltEtcAmt", fieldName: "swageImpdFcltEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "etcWholAmt", fieldName: "etcWholAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcAcdntCrfwAmt", fieldName: "etcAcdntCrfwAmt", header: { text: "사고\n이월액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcNrcpAmt", fieldName: "etcNrcpAmt", header: { text: "미수납액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcAmt", fieldName: "etcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "crfwAmt", + + { + name: "purposeGroup", + header: { text: "용도별사업비(백만원)" }, + items: [ + + "usgByWholPjtcoAmt", + { name: "t1Group", header: { text: "처리장" }, items: ["tmplntWholAmt", "tmplntFcltAmt", "tmplntMendngAmt", "tmplntOperKeepAmt", "tmplntEtcAmt"] }, + { name: "t2Group", header: { text: "하수관로" }, items: ["wholSwagePplAmt", "swagePplFcltAmt", "swagePplMendngAmt", "swagePplOperKeepAmt", "swagePplEtcAmt"] }, + { name: "t3Group", header: { text: "유수지 및 배수펌프장" }, items: ["wholSwageImdpFcltAmt", "swageImpdFcltAmt", "swageImpdMendngAmt", "swageImpdOperKeepAmt", "swageImpdFcltEtcAmt"] }, + { name: "t4Group", header: { text: "기타" }, items: ["etcWholAmt", "etcAcdntCrfwAmt", "etcNrcpAmt", "etcAmt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts new file mode 100644 index 000000000..af0afc329 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts @@ -0,0 +1,38 @@ +import { ValueType } from "realgrid"; + +export const SewerageOrgCauseTab4ColumnConfig = { + tabKey: "ST00090004", + tabLabel: "원인자부담금", + rowStartNum: 3, + fixedColumns: 3, + 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 }, + + { name: "levyEvntcsCnt", fieldName: "levyEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "levyAmt", fieldName: "levyAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "trgtByWholClctnEvntcsCnt", fieldName: "trgtByWholClctnEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "trgtByWholClctnAmt", fieldName: "trgtByWholClctnAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "mltvEqpmEvntcsCnt", fieldName: "mltvEqpmEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mltvEqpmAmt", fieldName: "mltvEqpmAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "othrCstrnOthrActrCnt", fieldName: "othrCstrnOthrActrCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "othrCstrnOthrActrAmt", fieldName: "othrCstrnOthrActrAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "levyGroup", header: { text: "부과액" }, items: ["levyEvntcsCnt", "levyAmt"] }, + { + name: "collectionStatusGroup", + header: { text: "대상별 징수현황" }, + items: [ + { name: "c1Group", header: { text: "계" }, items: ["trgtByWholClctnEvntcsCnt", "trgtByWholClctnAmt"] }, + { name: "c2Group", header: { text: "배수설비설치자\n(하수도법 제61조 제1항)" }, items: ["mltvEqpmEvntcsCnt", "mltvEqpmAmt"] }, + { name: "c3Group", header: { text: "타공사·타행위자\n(하수도법 제61조 제2항)" }, items: ["othrCstrnOthrActrCnt", "othrCstrnOthrActrAmt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts new file mode 100644 index 000000000..5bfe56b94 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts @@ -0,0 +1,62 @@ +import { ValueType } from "realgrid"; + +export const SewerageUsageFeeTab3ColumnConfig = { + tabKey: "ST00090003", + tabLabel: "하수도 사용료", + rowStartNum: 3, + fixedColumns: 3, + 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 }, + + { name: "levyAmt", fieldName: "levyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "levyQnt", fieldName: "levyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 가정용 + { name: "hshusLevyAmt", fieldName: "hshusLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "hshusLevyQnt", fieldName: "hshusLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "hshusUntprcAmt", fieldName: "hshusUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 업무용 + { name: "taskFrLevyAmt", fieldName: "taskFrLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "taskFrLevyQnt", fieldName: "taskFrLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "taskFrUntprcAmt", fieldName: "taskFrUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 영업용(일반용) + { name: "bsnGnrlLevyAmt", fieldName: "bsnGnrlLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bsnGnrlLevyQnt", fieldName: "bsnGnrlLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bsnGnrlUntprcAmt", fieldName: "bsnGnrlUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 욕탕 1종 + { name: "massBthhsFrLevyAmt", fieldName: "massBthhsFrLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "massBthhsFrLevyQnt", fieldName: "massBthhsFrLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "massBthhsFrUntprcAmt", fieldName: "massBthhsFrUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 욕탕 2종 + { name: "bthhsFrSpcs2LevyAmt", fieldName: "bthhsFrSpcs2LevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bthhsFrSpcs2LevyQnt", fieldName: "bthhsFrSpcs2LevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bthhsFrSpcs2UntprcAmt", fieldName: "bthhsFrSpcs2UntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 산업 및 공업용 + { name: "induMdsrLevyAmt", fieldName: "induMdsrLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "induMdsrLevyQnt", fieldName: "induMdsrLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "induMdsrUntprcAmt", fieldName: "induMdsrUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 기타 + { name: "etcLevyAmt", fieldName: "etcLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcLevyQnt", fieldName: "etcLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcLevyUntprcAmt", fieldName: "etcLevyUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "totalGroup", header: { text: "총계" }, items: ["levyAmt", "levyQnt"] }, + { name: "g1Group", header: { text: "가정용" }, items: ["hshusLevyAmt", "hshusLevyQnt", "hshusUntprcAmt"] }, + { name: "g2Group", header: { text: "업무용" }, items: ["taskFrLevyAmt", "taskFrLevyQnt", "taskFrUntprcAmt"] }, + { name: "g3Group", header: { text: "영업용(일반용)" }, items: ["bsnGnrlLevyAmt", "bsnGnrlLevyQnt", "bsnGnrlUntprcAmt"] }, + { name: "g4Group", header: { text: "욕탕 1종(대중탕용)" }, items: ["massBthhsFrLevyAmt", "massBthhsFrLevyQnt", "massBthhsFrUntprcAmt"] }, + { name: "g5Group", header: { text: "욕탕2종" }, items: ["bthhsFrSpcs2LevyAmt", "bthhsFrSpcs2LevyQnt", "bthhsFrSpcs2UntprcAmt"] }, + { name: "g6Group", header: { text: "산업 및 공업용" }, items: ["induMdsrLevyAmt", "induMdsrLevyQnt", "induMdsrUntprcAmt"] }, + { name: "g7Group", header: { text: "기타" }, items: ["etcLevyAmt", "etcLevyQnt", "etcLevyUntprcAmt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts new file mode 100644 index 000000000..1a430a911 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts @@ -0,0 +1,121 @@ +/** 09. 하수도 재정관리 검색 조건 공통 파라미터 */ +export interface SewerageFinanceSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 하수도재정 결산 목록 항목 */ +export interface SewerageFinanceSettlementTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fnnByWholPjtcoAmt: number; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + wholSwagePplAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + wholSwageImdpFcltAmt: number; // 스펙 대조 자동 매핑 보정 (SwageImpdFclt) + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; +} + +/** [Tab2] 하수도 요금 목록 항목 */ +export interface SewerageFeeTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; +} + +/** [Tab3] 하수도 사용료 목록 항목 */ +export interface SewerageUsageFeeTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + levyAmt: number; + levyQnt: number; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + bsnGnrlLevyAmt: number; + bsnGnrlLevyQnt: number; + bsnGnrlUntprcAmt: number; + massBthhsFrLevyAmt: number; + massBthhsFrLevyQnt: number; + massBthhsFrUntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induMdsrLevyAmt: number; + induMdsrLevyQnt: number; + induMdsrUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; +} + +/** [Tab4] 원인자 부담금 목록 항목 */ +export interface SewerageOrgCauseTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; +} + +export interface SewerageFinanceSettlementResponse { status: string; message?: string; data: SewerageFinanceSettlementTotalItem[]; } +export interface SewerageFeeResponse { status: string; message?: string; data: SewerageFeeTotalItem[]; } +export interface SewerageUsageFeeResponse { status: string; message?: string; data: SewerageUsageFeeTotalItem[]; } +export interface SewerageOrgCauseResponse { status: string; message?: string; data: SewerageOrgCauseTotalItem[]; } \ No newline at end of file From d4c9989069dfb19608d4dfeb079216ab34a74160 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:56:20 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20[=EC=A7=91=EA=B3=84=ED=91=9C]=20?= =?UTF-8?q?=EB=AC=BC=EC=9E=AC=EC=9D=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/total/WaterReuseTot.tsx | 225 +++++++++++++++++- .../SewerageGrayWaterTab2Column.ts | 48 ++++ .../SewerageRainwaterTab3Column.ts | 33 +++ .../sewerageReuse/SewerageReuseTab1Column.ts | 89 +++++++ .../SewerageWwtRtlztnRegTab4Column.ts | 28 +++ 5 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts create mode 100644 apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts diff --git a/apps/st/src/components/statistics/total/WaterReuseTot.tsx b/apps/st/src/components/statistics/total/WaterReuseTot.tsx index 567797983..e48d12673 100644 --- a/apps/st/src/components/statistics/total/WaterReuseTot.tsx +++ b/apps/st/src/components/statistics/total/WaterReuseTot.tsx @@ -1,10 +1,229 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + import SearchForm from "../../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 { toGridRows } from "@/components/util/grid/gridFunction"; + +// [폴더 정밀 정비 완료] 프로젝트 트리 구조 5개 컬럼 스펙 패키지 호출 +import { SewerageReuseTab1ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageReuseTab1Column"; +import { SewerageGrayWaterTab2ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column"; +import { SewerageRainwaterTab3ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column"; +import { SewerageWwtRtlztnRegTab4ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column"; +import { SewerageWwtRtlztnSncnTab5ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column"; + +// 집계표(TOTAL) API 서비스 및 인터페이스 명세 임포트 +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + WaterReuseSummaryRequest +} from "@/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface"; +import CellColorInfo from "./CellColorInfo"; + +// 5개 멀티 탭 매핑을 위한 설정 취합 배열 +const WaterReuseColumnListTot = [ + SewerageReuseTab1ColumnConfig, + SewerageGrayWaterTab2ColumnConfig, + SewerageRainwaterTab3ColumnConfig, + SewerageWwtRtlztnRegTab4ColumnConfig, + SewerageWwtRtlztnSncnTab5ColumnConfig // 👈 5번째 탭 풀장착 완료 +]; + +const firstTabKey = WaterReuseColumnListTot[0]?.tabKey ?? "ST00040007_1"; const WaterReuseTot = () => { + const { setMessage: alert } = useAlertStore((state) => state); + + // Map 데이터 구조를 사용하여 완벽하게 안전한 멀티 탭 데이터 상태 유지 + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = WaterReuseColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + const param: WaterReuseSummaryRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + // [TAB1] 공공하수처리수 재이용 현황 조회 + if (tabKey === "ST00100001") { + const response = await statisticsTotalStatusApi.getSewerageReuseTab1List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [TAB2] 중수도 현황 조회 + if (tabKey === "ST00100002") { + const response = await statisticsTotalStatusApi.getGrayWaterStatusTab2List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [TAB3] 빗물이용시설현황 조회 + if (tabKey === "ST00100003") { + const response = await statisticsTotalStatusApi.getRainwaterStatusTab3List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [TAB4] 하·폐수처리수 재이용시설 시공업 등록현황 조회 + if (tabKey === "ST00100004") { + const response = await statisticsTotalStatusApi.getWwtRtlztnRegStatusTab4List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + + // [TAB5] 하·폐수처리수 재이용시설 시공업 인가현황 조회 + if (tabKey === "ST00100005") { + const response = await statisticsTotalStatusApi.getWwtRtlztnSncnStatusTab5List(param); + if (response && response.status === "success" && response.data) { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } + }; + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + const ok = await loadGridData(activeTab, values); + 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; + + // 중복 API 통신 방지 필터 캐싱 디스패치 + const existing = rowDataMap.get(tabKey); + if (existing && existing.length > 0) return; + + try { + setIsLoading(true); + await loadGridData(tabKey, values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {/* 정중앙 5rem 크기 고정 격리형 로딩 애니메이션 */} + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + 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={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts new file mode 100644 index 000000000..6987edb66 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts @@ -0,0 +1,48 @@ +import { ValueType } from "realgrid"; + +export const SewerageGrayWaterTab2ColumnConfig = { + tabKey: "ST00100002", + tabLabel: "중수도 현황", + rowStartNum: 2, // 2단 그룹 헤더이므로 rowStartNum은 2로 지정합니다. + fixedColumns: 1, + 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 }, + + // 단일 마스터 컬럼들 (image_103ea5 스펙 반영) + { name: "fcltNm", fieldName: "fcltNm", header: { text: "건축물명/\n개발사업명" }, editable: false }, + { name: "bldgSeCn", fieldName: "bldgSeCn", header: { text: "시설물용도/\n개발사업종류" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "주소" }, editable: false }, + { name: "lglTrgtYn", fieldName: "lglTrgtYn", header: { text: "법적의무\n대상여부\n(대상/미대상)" }, editable: false }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "설치완료일\nyyyy-mm-dd" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일\nyyyy-mm-dd" }, editable: false }, + + // 시설현황 그룹 소속 + { name: "archWholArea", fieldName: "archWholArea", header: { text: "건축연면적\n(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsCpct", fieldName: "prcsCpct", header: { text: "처리용량\n(㎡/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 이용 현황 소속 컬럼들 + { name: "wtrUseQnt", fieldName: "wtrUseQnt", header: { text: "물 사용량\n(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "ruswtrUtztnQnt", fieldName: "ruswtrUtztnQnt", header: { text: "중수도\n이용량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sbstCntUtztnQnt", fieldName: "sbstCntUtztnQnt", header: { text: "대체수\n이용량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mnUsgCn", fieldName: "mnUsgCn", header: { text: "중수도용도" }, editable: false }, + { name: "ruswtrOperAmt", fieldName: "ruswtrOperAmt", header: { text: "연간운영비\n(백만원/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "bldgSeCn", + "fcltPstnCn", + "lglTrgtYn", + "cmptnStrtYmd", + "oprtngStrtYmd", + { name: "facilityStatusGroup", header: { text: "시설현황" }, items: ["archWholArea", "prcsCpct"] }, + "wtrUseQnt", + "ruswtrUtztnQnt", + "sbstCntUtztnQnt", + "mnUsgCn", + "ruswtrOperAmt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts new file mode 100644 index 000000000..84d3d0a7e --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts @@ -0,0 +1,33 @@ +import { ValueType } from "realgrid"; + +export const SewerageRainwaterTab3ColumnConfig = { + tabKey: "ST00100003", + tabLabel: "빗물이용시설현황", + rowStartNum: 2, + fixedColumns: 3, + 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 }, + + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "bdstTypCn", fieldName: "bdstTypCn", header: { text: "건축물용도" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치(주소)" }, editable: false }, + { name: "lglTrgtYn", fieldName: "lglTrgtYn", header: { text: "법적의무 대상여부\n(대상/미대상)" }, editable: false }, + { name: "instlCmptnYmd", fieldName: "instlCmptnYmd", header: { text: "설치완료일\n(yyyy-mm-dd)" }, editable: false }, + { name: "instlAmt", fieldName: "instlAmt", header: { text: "설치비\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rcptMynCn", fieldName: "rcptMynCn", header: { text: "집수면 종류" }, editable: false }, + { name: "filtrPrcsFcltYnExpln", fieldName: "filtrPrcsFcltYnExpln", header: { text: "여과등 처리시설 유무" }, editable: false }, + { name: "impdCpct", fieldName: "impdCpct", header: { text: "저류조 용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 이용 현황 그룹 소속 + { name: "anlUseQnt", fieldName: "anlUseQnt", header: { text: "연간 이용량\n(㎥/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnUtlzUsgExpln", fieldName: "exlnUtlzUsgExpln", header: { text: "이용용도" }, editable: false }, + { name: "anlOperAmt", fieldName: "anlOperAmt", header: { text: "연간운영비\n(백만원/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", "bdstTypCn", "fcltPstnCn", "lglTrgtYn", "instlCmptnYmd", "instlAmt", "rcptMynCn", "filtrPrcsFcltYnExpln", "impdCpct", + "anlUseQnt", "exlnUtlzUsgExpln", "anlOperAmt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts new file mode 100644 index 000000000..6534fa651 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts @@ -0,0 +1,89 @@ +import { ValueType } from "realgrid"; + +export const SewerageReuseTab1ColumnConfig = { + tabKey: "ST00100001", + tabLabel: "공공하수처리수 재이용", + rowStartNum: 4, // 3단 계층 다단 헤더 구조이므로 4로 유지합니다. + fixedColumns: 1, + 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 }, + + // 하수처리장 + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "시설명" }, editable: false }, + { name: "sncnYmd", fieldName: "sncnYmd", header: { text: "인가일\n(년/월/일)" }, editable: false }, + { name: "prcsFcltCpct", fieldName: "prcsFcltCpct", header: { text: "시설용량\n(㎥/일)" }, editable: false }, + { name: "prcsQnt", fieldName: "prcsQnt", header: { text: "연간\n하수처리량\n(천톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "lglYn", fieldName: "lglYn", header: { text: "법적의무대상\n(대상/미대상)" }, editable: false }, + + // 재이용시설 설치시 소속 + { name: "instlYn", fieldName: "instlYn", header: { text: "설치유무\n(유/무)" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일\nyyyy-mm-dd" }, editable: false }, + { name: "instlAprvYmd", fieldName: "instlAprvYmd", header: { text: "설치승인일\nyyyy-mm-dd" }, editable: false }, + { name: "rprcsFcltCpct", fieldName: "rprcsFcltCpct", header: { text: "용량(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "mtcnsNm", fieldName: "mtcnsNm", header: { text: "공법/기자재" }, editable: false }, + { name: "ruswtPplLen", fieldName: "ruswtPplLen", header: { text: "공급관로길이\n(km)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "ruswtPumpLen", fieldName: "ruswtPumpLen", header: { text: "공급펌프\n(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 하수처리수 재이용현황 + { name: "wholUseQnt", fieldName: "wholUseQnt", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlWshwtrQnt", fieldName: "ithlWshwtrQnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlClnQty", fieldName: "ithlClnQty", header: { text: "세척수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlFrzgQty", fieldName: "ithlFrzgQty", header: { text: "냉각수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlLdhcUswtrQnt", fieldName: "ithlLdhcUswtrQnt", header: { text: "청소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlDltnUswtrQnt", fieldName: "ithlDltnUswtrQnt", header: { text: "조경용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlRuswtrQnt", fieldName: "ithlRuswtrQnt", header: { text: "희석용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlEtcQnt", fieldName: "ithlEtcQnt", header: { text: "장내중수도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlUnsdQnt", fieldName: "ithlUnsdQnt", header: { text: "장내기타용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "otcntrUswtrWhol", fieldName: "otcntrUswtrWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrClnRstrmQnt", fieldName: "otcntrClnRstrmQnt", header: { text: "청소·화장실\n용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrWshwtrQnt", fieldName: "otcntrWshwtrQnt", header: { text: "세척·살수용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrLdhcUswtrQnt", fieldName: "otcntrLdhcUswtrQnt", header: { text: "조경용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrHdrplUswtrQnt", fieldName: "otcntrHdrplUswtrQnt", header: { text: "친수용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrRvrKeepQnt", fieldName: "otcntrRvrKeepQnt", header: { text: "하천유지용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "agrcltUswtrPstnQnt", fieldName: "agrcltUswtrPstnQnt", header: { text: "농업용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ugwtrChargQnt", fieldName: "ugwtrChargQnt", header: { text: "지하수충전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "mdsrUswtrQnt", fieldName: "mdsrUswtrQnt", header: { text: "공업용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "rtlztnRt", fieldName: "rtlztnRt", header: { text: "재이용률\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "seweragePlantGroup", header: { text: "하수처리장" }, items: ["swtrmtFcltNm", "sncnYmd", "prcsFcltCpct", "prcsQnt", "lglYn"] }, + { + name: "reuseFacilityGroup", + header: { text: "재이용시설" }, + items: [ + "instlYn", + { + name: "installationGroup", + header: { text: "재이용시설 설치시" }, + items: [ + "oprtngStrtYmd", + "instlAprvYmd", + { + // ★ 핵심 교정: 이미지 명세에 맞춰 내부에 서브 그룹 헤더 추가 완료 + name: "reProcessingFacilityGroup", + header: { text: "재처리시설" }, + items: ["rprcsFcltCpct", "mtcnsNm"] + }, + "ruswtPplLen", + "ruswtPumpLen" + ] + } + ] + }, + { + name: "reuseStatusGroup", + header: { text: "하수처리수 재이용현황(천톤/년)" }, + items: [ + "wholUseQnt", + { name: "internalWaterGroup", header: { text: "장내 용수" }, items: ["ithlWshwtrQnt", "ithlClnQty", "ithlFrzgQty", "ithlLdhcUswtrQnt", "ithlDltnUswtrQnt", "ithlRuswtrQnt", "ithlEtcQnt", "ithlUnsdQnt"] }, + { name: "externalWaterGroup", header: { text: "장외 용수" }, items: ["otcntrUswtrWhol", "otcntrClnRstrmQnt", "otcntrWshwtrQnt", "otcntrLdhcUswtrQnt", "otcntrHdrplUswtrQnt", "otcntrRvrKeepQnt", "agrcltUswtrPstnQnt", "ugwtrChargQnt", "mdsrUswtrQnt"] } + ] + }, + "rtlztnRt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts new file mode 100644 index 000000000..2d51a5354 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts @@ -0,0 +1,28 @@ + + + +export const SewerageWwtRtlztnRegTab4ColumnConfig = { + tabKey: "ST00100004", + tabLabel: "하·폐수처리수재이용시설설계시공업등록현황", + rowStartNum: 2, // 행정구역 2단 묶음 헤더 구조이므로 2로 지정합니다. + fixedColumns: 1, + 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 }, + + // 메인 마스터 문자열 지표들 (화면 그리드 명세서 순서 100% 매칭) + { name: "bplcNm", fieldName: "bplcNm", header: { text: "상호(명칭)" }, editable: false }, + { name: "rprsvNm", fieldName: "rprsvNm", header: { text: "대표자" }, editable: false }, + { name: "bplcPstnCn", fieldName: "bplcPstnCn", header: { text: "주소" }, editable: false }, + { name: "regDt", fieldName: "regDt", header: { text: "등록일\nyyyy-mm-dd" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "regDt" + ] +}; \ No newline at end of file From 84ae60edd6c8f2d3e474c17235d65afa6577a5fd Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:57:39 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]=2011?= =?UTF-8?q?=ED=95=98=EC=88=98=EB=B0=8F=20=EB=B6=84=EB=87=A8=EC=B0=8C?= =?UTF-8?q?=EA=BA=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/SewageExcretaSlurgeTot.tsx | 206 +++++++++++++++++- .../DigestionTankTab2Column.ts | 42 ++++ .../HumanWasteSludgeTab4Column.ts | 37 ++++ .../SewageSludgeOperTab3Column.ts | 30 +++ .../SludgeProcessingTab1Column.ts | 103 +++++++++ .../statistics-sludge-total-interface.ts | 168 ++++++++++++++ 6 files changed, 583 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts create mode 100644 apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts create mode 100644 apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts create mode 100644 apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts diff --git a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx index c384f1461..c251aff5c 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx @@ -1,10 +1,210 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + import SearchForm from "@/components/module/SearchForm"; +import DataGrid from "@/components/module/DataGrid"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +// 4개 탭 컬럼 설정 임포트 +import { SludgeProcessingTab1ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column"; +import { DigestionTankTab2ColumnConfig } from "@/components/util/column/total/sludgeProcessing/DigestionTankTab2Column"; +import { SewageSludgeOperTab3ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column"; +import { HumanWasteSludgeTab4ColumnConfig } from "@/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column"; + +// API 인스턴스 및 인터페이스 임포트 (타입 안전성 확보) +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + SludgeSummaryRequest, + SludgeProcessingTab1Response, + DigestionTankOperTab2Response, + SewageSludgeOperTab3Response, + HumanWasteSludgeTab4Response +} from "@/entities/statistics/model/statistics-total/statistics-sludge-total-interface"; +import CellColorInfo from "./CellColorInfo"; + +// 탭 순회 설정 +const SludgeColumnListTot = [ + SludgeProcessingTab1ColumnConfig, + DigestionTankTab2ColumnConfig, + SewageSludgeOperTab3ColumnConfig, + HumanWasteSludgeTab4ColumnConfig +].filter(Boolean); + +const firstTabKey = SludgeColumnListTot[0]?.tabKey ?? "ST00110001"; const SewageExcretaSlurgeTot = () => { + 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 = SludgeColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + +const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + setIsLoading(true); + const param: SludgeSummaryRequest = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + // [TAB1] 하수및분뇨찌꺼기 > 찌꺼기처리시설현황 + if (tabKey === "ST00110001") { + const response: SludgeProcessingTab1Response = await statisticsTotalStatusApi.getSludgeProcessingTab1List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + // [TAB2] 소화조 운영현황 + else if (tabKey === "ST00110002") { + const response: DigestionTankOperTab2Response = await statisticsTotalStatusApi.getSludgeTab2List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + // [TAB3] 하수찌꺼기 처리시설 운영현황 + else if (tabKey === "ST00110003") { + const response: SewageSludgeOperTab3Response = await statisticsTotalStatusApi.getSludgeTab3List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + // [TAB4] 분뇨찌꺼기 처리현황 + else if (tabKey === "ST00110004") { + const response: HumanWasteSludgeTab4Response = await statisticsTotalStatusApi.getSludgeTab4List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + + alert("데이터 조회 중 오류가 발생했습니다."); + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } finally { + setIsLoading(false); + } + }; + + /** 검색 버튼 이벤트: SearchForm의 Values 타입을 기반으로 호출 */ + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + // 탭 변경 없이 현재 탭(activeTab)에 대해서만 명시적 타입 호출 + const ok = await loadGridData(activeTab, values); + if (ok) { + lastSearchedValuesRef.current = values; + } else { + lastSearchedValuesRef.current = null; + } + } catch (error) { + alert("데이터 조회 중 오류가 발생했습니다."); + lastSearchedValuesRef.current = null; + } finally { + setIsLoading(false); + } + }; + + /** 탭 변경 이벤트: 현재 탭 상태와 SearchForm의 마지막 값을 동기화 */ + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + // 마지막 검색 값이 있으면 해당 탭만 새로 로드 + const values = lastSearchedValuesRef.current; + if (!values || rowDataMap.has(tabKey)) return; + + try { + setIsLoading(true); + await loadGridData(tabKey, values); + } catch (error) { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + {info.tabLabel} + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + + 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={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts new file mode 100644 index 000000000..65b1c50bc --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts @@ -0,0 +1,42 @@ +import { ValueType } from "realgrid"; + +export const DigestionTankTab2ColumnConfig = { + tabKey: "ST00110002", + tabLabel: "소화조 운영현황", + rowStartNum: 3, // 행정구역, 하수처리장명 등 기본 정보 이후 다단 헤더가 있으므로 3단 지정 + fixedColumns: 1, + 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 }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "하수\n처리장명" }, editable: false }, + + { name: "digesterYn", fieldName: "digesterYn", header: { text: "소화조 유/무\n(Y/N)선택" }, editable: false }, + { name: "dsgnCpct", fieldName: "dsgnCpct", header: { text: "설계\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "actlOperCpct", fieldName: "actlOperCpct", header: { text: "실제운전\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "inputQnt", fieldName: "inputQnt", header: { text: "투입량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tsConc", fieldName: "tsConc", header: { text: "투입 TS 농도\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "oprtngDays", fieldName: "oprtngDays", header: { text: "가동일수\n(연간)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "gnrtGasQnt", fieldName: "gnrtGasQnt", header: { text: "발생가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 소화가스 이용현황(하위 항목) + { name: "gasTotal", fieldName: "gasTotal", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "gasSale", fieldName: "gasSale", header: { text: "판매" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "gasGen", fieldName: "gasGen", header: { text: "발전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "gasSelf", fieldName: "gasSelf", header: { text: "자체활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "gasEtc", fieldName: "gasEtc", header: { text: "기타(CNG등)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "surplusGas", fieldName: "surplusGas", header: { text: "잉여가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + { name: "operGroup", header: { text: "소화조 운영현황" }, items: ["digesterYn", "dsgnCpct", "actlOperCpct", "inputQnt", "tsConc", "oprtngDays", "gnrtGasQnt", + { + name: "gasUsageGroup", + header: { text: "소화가스 이용현황(㎥/일)" }, + items: ["gasTotal", "gasSale", "gasGen", "gasSelf", "gasEtc"] + },"surplusGas" + ] }, + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts new file mode 100644 index 000000000..df6a57742 --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts @@ -0,0 +1,37 @@ +import { ValueType } from "realgrid"; + +export const HumanWasteSludgeTab4ColumnConfig = { + tabKey: "ST00110004", + tabLabel: "분뇨찌꺼기(슬러지)처리현황", + rowStartNum: 2, // 행정구역 2단 레이아웃 + fixedColumns: 1, + 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 }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "분뇨\n처리장명" }, editable: false }, + + { name: "gnam", fieldName: "gnam", header: { text: "발생량\n(A)=(B)+(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wholDspsQnt", fieldName: "wholDspsQnt", header: { text: "계(B)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rcycQnt", fieldName: "rcycQnt", header: { text: "재활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "olandLfvlm", fieldName: "olandLfvlm", header: { text: "육상매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrQnt", fieldName: "incnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "oceanInputQnt", fieldName: "oceanInputQnt", header: { text: "해양투기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelQnt", fieldName: "fuelQnt", header: { text: "연료화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcQnt", fieldName: "etcQnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "nondspsQnt", fieldName: "nondspsQnt", header: { text: "미처분량\n(이월량)(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "타시설\n연계여부" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "gnam", + { + name: "dspsGroup", + header: { text: "처분량" }, + items: ["wholDspsQnt", "rcycQnt", "olandLfvlm", "incnrQnt", "oceanInputQnt", "fuelQnt", "etcQnt"] + }, + "nondspsQnt", + "swtrmtFcltNm" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts new file mode 100644 index 000000000..c7a5786ce --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts @@ -0,0 +1,30 @@ +import { ValueType } from "realgrid"; + +export const SewageSludgeOperTab3ColumnConfig = { + tabKey: "ST00110003", + tabLabel: "하수찌꺼기 처리시설 운영현황", + rowStartNum: 2, + fixedColumns: 1, + 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 }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "찌꺼기처리\n시설명" }, editable: false }, + + { name: "prcsMthExpln", fieldName: "prcsMthExpln", header: { text: "처리방식" }, editable: false }, + { name: "fcltCpct", fieldName: "fcltCpct", header: { text: "시설용량\n(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "anlPrcsQnt", fieldName: "anlPrcsQnt", header: { text: "연간처리량\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dayAvgOprtngHr", fieldName: "dayAvgOprtngHr", header: { text: "일평균\n가동시간\n(1~24)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "anlOprtngDayCnt", fieldName: "anlOprtngDayCnt", header: { text: "연중 가동일\n(1~330)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "oprtngRt", fieldName: "oprtngRt", header: { text: "가동율(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + { + name: "operStatusGroup", + header: { text: "처리시설 운영현황" }, + items: ["prcsMthExpln", "fcltCpct", "anlPrcsQnt", "dayAvgOprtngHr", "anlOprtngDayCnt", "oprtngRt"] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts new file mode 100644 index 000000000..d61e6a535 --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts @@ -0,0 +1,103 @@ +import { ValueType } from "realgrid"; + +export const SludgeProcessingTab1ColumnConfig = { + tabKey: "ST00110001", + tabLabel: "찌꺼기(슬러지)처리시설현황", + rowStartNum: 4, // 3단 그룹 대단위 레이아웃 명세이므로 4로 바인딩합니다. + fixedColumns: 1, + 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 }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "하수\n처리장명" }, editable: false }, + + // 찌꺼기발생량 + { name: "gnamWhol", fieldName: "gnamWhol", header: { text: "합계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "itslfQnt", fieldName: "itslfQnt", header: { text: "자체" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdInflwQnt", fieldName: "otsdInflwQnt", header: { text: "외부유입량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 이송 및 함수율 지표들 + { name: "transfTmplntNm", fieldName: "transfTmplntNm", header: { text: "이송처리장" }, editable: false }, + { name: "transfPrcsQnt", fieldName: "transfPrcsQnt", header: { text: "이송처리량\n(㎥/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "srpFncRt", fieldName: "srpFncRt", header: { text: "함수율\n(%, 탈수기준)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wholSrpPrcsQnt", fieldName: "wholSrpPrcsQnt", header: { text: "찌꺼기\n처리량 합계\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 자체 처리 1차 대문 항목들 + { name: "itslfTs1Whol", fieldName: "itslfTs1Whol", header: { text: "자체찌꺼기\n처리량 소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "itslfTs1RclmQnt", fieldName: "itslfTs1RclmQnt", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 소각 후 처리(2차) 서브 항목들 + { name: "itslfTs1IncnrQnt", fieldName: "itslfTs1IncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsWhol", fieldName: "incnrPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsLfvlm", fieldName: "incnrPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsPrdctRmtrlQnt", fieldName: "incnrPrcsPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsFmlyrQnt", fieldName: "incnrPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsFcltNm", fieldName: "incnrPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 고화 처리군 + { name: "itslfTs1AltdQnt", fieldName: "itslfTs1AltdQnt", header: { text: "고화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsWhol", fieldName: "stbznPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsLfvlm", fieldName: "stbznPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsFmlyrQnt", fieldName: "stbznPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsFcltNm", fieldName: "stbznPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 건조 처리군 + { name: "itslfTs1DryQnt", fieldName: "itslfTs1DryQnt", header: { text: "건조" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsWhol", fieldName: "dryPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsLfvlm", fieldName: "dryPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsIncnrQnt", fieldName: "dryPrcsIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFuelQnt", fieldName: "dryPrcsFuelQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsPrdctRmtrlQnt", fieldName: "dryPrcsPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFmlyrQnt", fieldName: "dryPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsCmpstgQnt", fieldName: "dryPrcsCmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFcltNm", fieldName: "dryPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 탄화 처리군 + { name: "itslfTs1FuelQnt", fieldName: "itslfTs1FuelQnt", header: { text: "탄화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsWhol", fieldName: "fuelPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsIncnrQnt", fieldName: "fuelPrcsIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsFuelQnt", fieldName: "fuelPrcsFuelQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsFcltNm", fieldName: "fuelPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 퇴비화 독립군 + { name: "itslfTs1CmpstgQnt", fieldName: "itslfTs1CmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "cmpsPrcsFcltBzentyNm", fieldName: "cmpsPrcsFcltBzentyNm", header: { text: "시설(업체명)" }, editable: false }, + + // 외부(민간) 위탁 처리현황 + { name: "otsdCnsgnWhol", fieldName: "otsdCnsgnWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnRclmQnt", fieldName: "otsdCnsgnRclmQnt", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnIncnrQnt", fieldName: "otsdCnsgnIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFuelPwplQnt", fieldName: "otsdCnsgnFuelPwplQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnPrdctRmtrlQnt", fieldName: "otsdCnsgnPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFmlyrQnt", fieldName: "otsdCnsgnFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnCmpstgQnt", fieldName: "otsdCnsgnCmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFcltNm", fieldName: "otsdCnsgnFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 수도권 광역 위탁 처리량 + { name: "cparTs2Whol", fieldName: "cparTs2Whol", header: { text: "수도권광역\n위탁처리량\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + { name: "gnamGroup", header: { text: "찌꺼기발생량(톤/년, 탈수기준)" }, items: ["gnamWhol", "itslfQnt", "otsdInflwQnt"] }, + "transfTmplntNm", "transfPrcsQnt", "srpFncRt", "wholSrpPrcsQnt", + { + name: "itslfProcessingGroup", + header: { text: "자체 찌꺼기 처리량 현황(톤/년)" }, + items: [ + "itslfTs1Whol", "itslfTs1RclmQnt","itslfTs1IncnrQnt", + { name: "incnrGroup", header: { text: "소각 후 처리(2차)" }, items: ["incnrPrcsWhol", "incnrPrcsLfvlm", "incnrPrcsPrdctRmtrlQnt", "incnrPrcsFmlyrQnt", "incnrPrcsFcltNm"] }, + "itslfTs1AltdQnt", + { name: "stbznGroup", header: { text: "고화 후 처리(2차)" }, items: [ "stbznPrcsWhol", "stbznPrcsLfvlm", "stbznPrcsFmlyrQnt", "stbznPrcsFcltNm"] }, + "itslfTs1DryQnt", + { name: "dryGroup", header: { text: "건조 후 처리(2차)" }, items: ["dryPrcsWhol", "dryPrcsLfvlm", "dryPrcsIncnrQnt", "dryPrcsFuelQnt", "dryPrcsPrdctRmtrlQnt", "dryPrcsFmlyrQnt", "dryPrcsCmpstgQnt", "dryPrcsFcltNm"] }, + "itslfTs1FuelQnt", + { name: "fuelGroup", header: { text: "탄화 후 처리(2차)" }, items: ["fuelPrcsWhol", "fuelPrcsIncnrQnt", "fuelPrcsFuelQnt", "fuelPrcsFcltNm"] }, + "itslfTs1CmpstgQnt", + { name: "cmpsGroup", header: { text: "퇴비화" }, items: ["cmpsPrcsFcltBzentyNm"] } + ] + }, + { name: "otsdGroup", header: { text: "외부(민간) 위탁 처리량(톤/년)" }, items: ["otsdCnsgnWhol", "otsdCnsgnRclmQnt", "otsdCnsgnIncnrQnt", "otsdCnsgnFuelPwplQnt", "otsdCnsgnPrdctRmtrlQnt", "otsdCnsgnFmlyrQnt", "otsdCnsgnCmpstgQnt", "otsdCnsgnFcltNm"] }, + "cparTs2Whol" + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts new file mode 100644 index 000000000..bae596f83 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts @@ -0,0 +1,168 @@ +/** 11. 하수및분뇨찌꺼기 공통 검색 파라미터 DTO 규격 */ +export interface SludgeSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [TAB1] 찌꺼기(슬러지) 처리시설 현황 데이터 행 항목 인터페이스 */ +export interface SludgeProcessingTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + + // 기본 정보 + fcltNm: string; + + // 찌꺼기 발생량(톤/년, 탈수기준) + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + + // 이송 및 함수율 + transfTmplntNm: string; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + + // 자체 찌꺼기 처리량 소계 및 매립 + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + + // 소각 후 처리(2차) + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string; + + // 고화 후 2차 + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string; + + // 건조 후 2차 + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string; + + // 탄화 후 2차 + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string; + + // 퇴비화 + itslfTs1CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string; + + // 외부(민간) 위탁 처리량 + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string; + + // 수도권광역 위탁 처리량 + cparTs2Whol: number; +} + +/** TAB1 서버 단일 응답 랩퍼 규격 */ +export interface SludgeProcessingTab1Response { + status: string; + message?: string; + data: SludgeProcessingTotalItem[]; +} +// ============================================================================ +// [TAB2] 소화조 운영현황 명세 +// ============================================================================ +export interface DigestionTankOperStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + rowType: string; + fcltNm: string; + digesterYn: string; + dsgnCpct: number; + actlOperCpct: number; + inputQnt: number; + tsConc: number; + oprtngDays: number; + gnrtGasQnt: number; + gasTotal: number; + gasSale: number; + gasGen: number; + gasSelf: number; + gasEtc: number; + surplusGas: number; +} +export interface DigestionTankOperTab2Response { status: string; message?: string; data: DigestionTankOperStatusTotalItem[]; } + +export interface SewageSludgeOperStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + prcsMthExpln: string; + fcltCpct: number; + anlPrcsQnt: number; + dayAvgOprtngHr: number; + anlOprtngDayCnt: number; + oprtngRt: number; +} + +export interface SewageSludgeOperTab3Response { + status: string; + message?: string; + data: SewageSludgeOperStatusTotalItem[]; +} + +export interface HumanWasteSludgeStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + gnam: number; + wholDspsQnt: number; + rcycQnt: number; + olandLfvlm: number; + incnrQnt: number; + oceanInputQnt: number; + fuelQnt: number; + etcQnt: number; + nondspsQnt: number; + swtrmtFcltNm: string; +} + +export interface HumanWasteSludgeTab4Response { + status: string; + message?: string; + data: HumanWasteSludgeStatusTotalItem[]; +} \ No newline at end of file From 64e5c26e8c5cf6c6dcac57c831cce6e484cdaf15 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 00:59:03 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20[=EC=A7=91=EA=B3=84=ED=91=9C]=201?= =?UTF-8?q?2.=ED=95=98=EC=88=98=EB=B0=8F=20=EB=B6=84=EB=87=A8=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=82=AC=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/SewageExcretaRelatedTot.tsx | 158 +++++++++++++++++- .../HumanWasteCollectionTab3Column.ts | 31 ++++ .../IndividualSewageTab4Column.ts | 25 +++ .../PublicHumanWasteStaffTab2Column.ts | 36 ++++ .../PublicSewageStaffTab1Column.ts | 35 ++++ 5 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts create mode 100644 apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts create mode 100644 apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts create mode 100644 apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts diff --git a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx index adcc810ad..4a3e1cc56 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx @@ -1,10 +1,162 @@ +import { useRef, useState } from "react"; +import { useAlertStore } from "@/shared/lib"; +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 { toGridRows } from "@/components/util/grid/gridFunction"; +import CellColorInfo from "./CellColorInfo"; + +// 12. 하수 및 분뇨 관리 산업 탭 설정 (경로 확인 필수) +import { PublicSewageStaffTab1ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column"; +import { PublicHumanWasteStaffTab2ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column"; +import { HumanWasteCollectionTab3ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column"; +import { IndividualSewageTab4ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column"; +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + SewerageIndustry, + PublicSewageStaffTab1Response, + PublicHumanWasteStaffTab2Response, + HumanWasteCollectionTab3Response, + IndividualSewageTab4Response +} from "@/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface"; + +const SewageExcretaIndustryColumnListTot = [PublicSewageStaffTab1ColumnConfig,PublicHumanWasteStaffTab2ColumnConfig,HumanWasteCollectionTab3ColumnConfig,IndividualSewageTab4ColumnConfig].filter(Boolean); +const firstTabKey = SewageExcretaIndustryColumnListTot[0]?.tabKey ?? "ST00120001"; const SewageExcretaRelatedTot = () => { + 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 handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + setIsLoading(true); + const param: SewerageIndustry = { + ctpvNm: values.sido === "ALL" ? "" : values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined + }; + + try { + if (tabKey === "ST00120001") { + const response: PublicSewageStaffTab1Response = await statisticsTotalStatusApi.getPublicSewageStaffTab1List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + else if (tabKey === "ST00120002") { + const response: PublicHumanWasteStaffTab2Response = await statisticsTotalStatusApi.getPublicHumanWasteStaffTab2List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + else if (tabKey === "ST00120003") { + const response: HumanWasteCollectionTab3Response = await statisticsTotalStatusApi.getHumanWasteCollectionTab3List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + // [TAB4] 개인하수처리관련업 + else if (tabKey === "ST00120004") { + const response: IndividualSewageTab4Response = await statisticsTotalStatusApi.getIndividualSewageTab4List(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + return false; + } catch { + alert("서버 연결 중 오류가 발생했습니다."); + return false; + } finally { + setIsLoading(false); + } + }; + + const handleSearch = async (values: SearchFormValues) => { + lastSearchedValuesRef.current = values; + await loadGridData(activeTab, values); + }; + + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + const values = lastSearchedValuesRef.current; + if (!values || rowDataMap.has(tabKey)) return; + await loadGridData(tabKey, values); + }; + return ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {SewageExcretaIndustryColumnListTot.map((info) => ( + {info.tabLabel} + ))} + + + {SewageExcretaIndustryColumnListTot.map((info) => ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(info.tabKey) ?? []; + const n = typeof updater === "function" ? updater(cur) : updater; + next.set(info.tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ))} + +
+
); }; diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts new file mode 100644 index 000000000..666c671b8 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts @@ -0,0 +1,31 @@ +import { ValueType } from "realgrid"; + +export const HumanWasteCollectionTab3ColumnConfig = { + tabKey: "ST00120003", + tabLabel: "분뇨수집·운반업", + rowStartNum: 3, + fixedColumns: 3, + 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 }, + { name: "bzentyCnt", fieldName: "bzentyCnt", header: { text: "업체수(개소)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "empNope", fieldName: "empNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "fcltSecrPrstCnt", fieldName: "fcltSecrPrstCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n3TonUdrCnt", fieldName: "n3TonUdrCnt", header: { text: "3톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n4TonUdrCnt", fieldName: "n4TonUdrCnt", header: { text: "4.5톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n8TonUdrCnt", fieldName: "n8TonUdrCnt", header: { text: "8톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "clctCnvncPrfmncQnt", fieldName: "clctCnvncPrfmncQnt", header: { text: "수집·운반실적(m³/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bzentyCnt", "empNope", + { + name: "fcltGroup", + header: { text: "시설(차량) 확보현황(대수)" }, + items: ["fcltSecrPrstCnt", "n3TonUdrCnt", "n4TonUdrCnt", "n8TonUdrCnt", "etcCnt"] + }, + "clctCnvncPrfmncQnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts new file mode 100644 index 000000000..18d99e765 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts @@ -0,0 +1,25 @@ +import { ValueType } from "realgrid"; + +export const IndividualSewageTab4ColumnConfig = { + tabKey: "ST00120004", + tabLabel: "개인하수처리관련업", + rowStartNum: 3, + fixedColumns: 1, + 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 }, + { name: "designMdsrCnt", fieldName: "designMdsrCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "designMdsrNope", fieldName: "designMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mfbizCnt", fieldName: "mfbizCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mfbizMdsrNope", fieldName: "mfbizMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mngCnt", fieldName: "mngCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mngMdsrNope", fieldName: "mngMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "designGroup", header: { text: "개인하수처리시설 설계·시공업" }, items: ["designMdsrCnt", "designMdsrNope"] }, + { name: "mfbizGroup", header: { text: "개인하수처리시설 제조업" }, items: ["mfbizCnt", "mfbizMdsrNope"] }, + { name: "mngGroup", header: { text: "개인하수처리시설 관리업" }, items: ["mngCnt", "mngMdsrNope"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts new file mode 100644 index 000000000..99d35f1ba --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts @@ -0,0 +1,36 @@ +import { ValueType } from "realgrid"; + +export const PublicHumanWasteStaffTab2ColumnConfig = { + tabKey: "ST00120002", + tabLabel: "공공기관 분뇨분야", + rowStartNum: 3, + fixedColumns: 2, + 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 }, + { name: "ognzNm", fieldName: "ognzNm", header: { text: "직제" }, editable: false }, + { name: "empGrssCnt", fieldName: "empGrssCnt", header: { text: "직원총수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pbadmsNope", fieldName: "pbadmsNope", header: { text: "행정직" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "techWholNope", fieldName: "techWholNope", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "engrkCnt", fieldName: "engrkCnt", header: { text: "토목" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mchnCnt", fieldName: "mchnCnt", header: { text: "기계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prvtmCnt", fieldName: "prvtmCnt", header: { text: "전기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "chmengCnt", fieldName: "chmengCnt", header: { text: "화공" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "envCnt", fieldName: "envCnt", header: { text: "환경" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "enjbEtcCnt", fieldName: "enjbEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techGroup", + header: { text: "기술직" }, + items: ["techWholNope", "engrkCnt", "mchnCnt", "prvtmCnt", "chmengCnt", "envCnt", "enjbEtcCnt"] + }, + "etcCnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts new file mode 100644 index 000000000..90c1a789b --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts @@ -0,0 +1,35 @@ +import { ValueType } from "realgrid"; + +export const PublicSewageStaffTab1ColumnConfig = { + tabKey: "ST00120001", + tabLabel: "공공기관 하수분야", + rowStartNum: 3, + fixedColumns: 2, + 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 }, + { name: "ognzNm", fieldName: "ognzNm", header: { text: "직제" }, editable: false }, + { name: "empGrssCnt", fieldName: "empGrssCnt", header: { text: "직원총수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pbadmsNope", fieldName: "pbadmsNope", header: { text: "행정직" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + // 기술직 + { name: "techWholNope", fieldName: "techWholNope", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "engrkCnt", fieldName: "engrkCnt", header: { text: "토목" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mchnCnt", fieldName: "mchnCnt", header: { text: "기계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prvtmCnt", fieldName: "prvtmCnt", header: { text: "전기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "chmengCnt", fieldName: "chmengCnt", header: { text: "화공" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "envCnt", fieldName: "envCnt", header: { text: "환경" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "enjbEtcCnt", fieldName: "enjbEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "ognzNm", "empGrssCnt", "pbadmsNope", + { + name: "techGroup", + header: { text: "기술직" }, + items: ["techWholNope", "engrkCnt", "mchnCnt", "prvtmCnt", "chmengCnt", "envCnt", "enjbEtcCnt"] + }, + "etcCnt" + ] +}; \ No newline at end of file From dae702404368cb648c89a5417c0429379722544b Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 01:00:31 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:[=EC=A7=91=EA=B3=84=ED=91=9C]13.?= =?UTF-8?q?=ED=95=98=EC=88=98=EB=8F=84=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=88=98=EC=A4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/SewerageServiceLevelTot.tsx | 234 +++++-------- .../ComplaintTotColumn.ts | 103 ++---- .../ResidentFriendlyFactTotColumn.ts | 105 ++---- .../api/statistics-total-service.ts | 331 +++++++++++++++++- ...tatistics-service-level-total-interface.ts | 17 +- 5 files changed, 478 insertions(+), 312 deletions(-) diff --git a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx index 452419dc2..a8e483c1d 100644 --- a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx +++ b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx @@ -1,131 +1,99 @@ 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 { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; -import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; - 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"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import CellColorInfo from "./CellColorInfo"; -const SewerageServiceLevelInpTabList = [ - ComplaintInpColumn, - ResidentFriendlyFactTotColumn, -]; -const SUPPORTED_TAB_KEYS = [ComplaintInpColumn.tabKey, ResidentFriendlyFactTotColumn.tabKey] as const; +// 13. 하수도서비스수준 컬럼 설정 +import { SewageComplaintTab1ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn"; +import { ResidentFriendlyFacilityTab2ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn"; + +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import type { + SewerageServiceLevelRequest, + SewerageServiceLevelComplaintResponse, + SewerageServiceLevelResidentFriendlyFactResponse +} from "@/entities/statistics/model/statistics-total/statistics-service-level-total-interface"; + +const SewageServiceLevelColumnListTot = [ + SewageComplaintTab1ColumnConfig, + ResidentFriendlyFacilityTab2ColumnConfig +].filter(Boolean); + +const firstTabKey = SewageServiceLevelColumnListTot[0]?.tabKey ?? "ST00130001"; 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 lastSearchValuesRef = useRef(null); - - const columnInfo = SewerageServiceLevelInpTabList; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); const handleReset = () => { setRowDataMap(new Map()); - setActiveTab(ComplaintInpColumn.tabKey); - lastSearchValuesRef.current = null; + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; }; - const loadSewerageServiceLevelTotTabData = async ( - tabKey: string, - values: SearchFormValues, - mergeIntoExistingMap: boolean, - ) => { - const param = { + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + setIsLoading(true); + const param: SewerageServiceLevelRequest = { ctpvNm: values.sido === "ALL" ? "" : values.sido, sggNm: values.gugun === "ALL" ? "" : values.gugun, sewerBzmnCd: values.bsnmCd, statsYr: values.year, rowDataYn: values.isDetail, + summaryYn: values.isSummary !== 'ALL', + summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined }; - 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); - - 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, rows); - return next; - }); - } else { - 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; + // [TAB1] 민원 + if (tabKey === "ST00130001") { + const response: SewerageServiceLevelComplaintResponse = await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } + } + // [TAB2] 주민친화시설 + else if (tabKey === "ST00130002") { + const response: SewerageServiceLevelResidentFriendlyFactResponse = await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param); + if (response?.status === "success") { + setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[]))); + return true; + } } + return false; } catch { - alert("데이터 조회 중 오류가 발생했습니다."); - lastSearchValuesRef.current = null; + alert("서버 연결 중 오류가 발생했습니다."); + return false; } finally { setIsLoading(false); } }; + const handleSearch = async (values: SearchFormValues) => { + lastSearchedValuesRef.current = values; + await loadGridData(activeTab, values); + }; + const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); - - 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); - } - } + const values = lastSearchedValuesRef.current; + if (!values || rowDataMap.has(tabKey)) return; + await loadGridData(tabKey, values); }; return (
- + + {isLoading && (
@@ -133,58 +101,48 @@ const SewerageServiceLevelTot = () => {
)} + + +
void handleActiveTabChange(v)} className="w-full" variant="default"> - {columnInfo.map((info) => ( - - {info.tabLabel || info.tabKey} - + {SewageServiceLevelColumnListTot.map((info) => ( + {info.tabLabel} ))} - {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"; - }} - /> - - ); - })} + + {SewageServiceLevelColumnListTot.map((info) => ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(info.tabKey) ?? []; + const n = typeof updater === "function" ? updater(cur) : updater; + next.set(info.tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport={true} + 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'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> + + ))}
diff --git a/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts b/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts index 8ed48c355..3d06e995a 100644 --- a/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts +++ b/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts @@ -1,77 +1,36 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원 -export const ComplaintInpColumn = { - columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } }, - { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } }, - { - name: "swtrmtPpltnCnt", - fieldName: "swtrmtPpltnCnt", - header: { text: "하수처리장민원건수(건)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "whol", - fieldName: "whol", - header: { text: "합계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관거" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "방류수수질" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "ldsCnt", fieldName: "ldsCnt", header: { text: "퇴적토" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "오염사고" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { - name: "crtrPpltnCvlcptEvntcsCnt", - fieldName: "crtrPpltnCvlcptEvntcsCnt", - header: { text: "인구10만명당 민원건수(건)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, - { name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false }, - { name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false }, - { name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false }, - ].map((col) => ({ ...col, editable: false })), - - columnLayout: [ - { - name: "groupA", - hideChildHeaders: false, - header: { text: "행정구역" }, - items: ["ctpvNm", "sggNm", "admdstNm"], - }, - { - name: "groupB", - hideChildHeaders: false, - header: { text: "민원건수(건)" }, - items: [ - "whol", - "odorCnt", - "swagePplCnt", - "dschgWqCnt", - "ldsCnt", - "plltnAcdntCnt", - "sewerCrgAmt", - "etcCnt", - ], - }, - "swtrmtPpltnCnt", - "crtrPpltnCvlcptEvntcsCnt", - "rmrk", - "statsYr", - "admdstCd", - "rowType", - ], - - rowStartNum: 2, - fixedColumns: 1, +export const SewageComplaintTab1ColumnConfig = { tabKey: "ST00130001", tabLabel: "민원", + rowStartNum: 3, // 행정구역 3단 헤더 + fixedColumns: 1, + 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 }, + { name: "swtrmtPpltnCnt", fieldName: "swtrmtPpltnCnt", header: { text: "하수처리인구\n(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 민원유형 + { name: "whol", fieldName: "whol", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관로" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "배수설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "ldsCnt", fieldName: "ldsCnt", header: { text: "맨홀" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "정화조" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + { name: "crtrPpltnCvlcptEvntcsCnt", fieldName: "crtrPpltnCvlcptEvntcsCnt", header: { text: "기준인구당\n민원건수\n(건/10,000명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtPpltnCnt", + { + name: "complaintGroup", + header: { text: "민원유형(건)" }, + items: ["whol", "odorCnt", "swagePplCnt", "dschgWqCnt", "ldsCnt", "plltnAcdntCnt", "sewerCrgAmt", "etcCnt"] + }, + "crtrPpltnCvlcptEvntcsCnt" + ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts b/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts index 1a121f49d..82890e8b9 100644 --- a/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts +++ b/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts @@ -1,81 +1,34 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설 -export const ResidentFriendlyFactTotColumn = { - columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } }, - { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } }, - { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" } }, - { - name: "fcltCpct", - fieldName: "fcltCpct", - header: { text: "시설용량(m³/일)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "tmplntArea", - fieldName: "tmplntArea", - header: { text: "처리장면적(㎡)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { - name: "whol", - fieldName: "whol", - header: { text: "총계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및 문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, - { name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false }, - { name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false }, - { name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false }, - ].map((col) => ({ ...col, editable: false })), - - columnLayout: [ - { - name: "groupA", - hideChildHeaders: false, - header: { text: "행정구역" }, - items: ["ctpvNm", "sggNm", "admdstNm"], - }, - "swtrmtFcltNm", - "fcltCpct", - "tmplntArea", - { - name: "groupB", - hideChildHeaders: false, - header: { text: "주민친화시설 면적(㎡)" }, - items: [ - "whol", - "brkFcltArea", - "childFrshpSpacArea", - "bdySportsFcltArea", - "sportsFcltArea", - "pkltArea", - "eduMultiFcltArea", - "wtlndMakeArea", - "etcCnt", - ], - }, - "rmrk", - "statsYr", - "admdstCd", - "rowType", - ], - - rowStartNum: 2, - fixedColumns: 3, +export const ResidentFriendlyFacilityTab2ColumnConfig = { tabKey: "ST00130002", tabLabel: "주민친화시설", + rowStartNum: 3, // 행정구역 3단 헤더 + fixedColumns: 1, + 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 }, + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" }, editable: false }, + { name: "tmplntArea", fieldName: "tmplntArea", header: { text: "처리장\n면적(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + // 주민친화시설 면적 + { name: "whol", fieldName: "whol", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및\n문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성\n(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtFcltNm", "tmplntArea", + { + name: "facilityGroup", + header: { text: "주민친화시설 면적(㎡)" }, + items: ["whol", "brkFcltArea", "childFrshpSpacArea", "bdySportsFcltArea", "sportsFcltArea", "pkltArea", "eduMultiFcltArea", "wtlndMakeArea", "etcCnt"] + } + ] }; \ 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 e5efbb39b..be7f839b9 100644 --- a/apps/st/src/entities/statistics/api/statistics-total-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-total-service.ts @@ -23,6 +23,62 @@ import type { import type { PublicSewageInquirySummaryResponse } from "../model/statistics-total/statistics-sewage-public-total-interface"; +import type { + SewerReservoirMaintainTotalRequest, + SewerReservoirStatusTotalResponse, + SewerPumpStationStatusTotalResponse +} from "../model/statistics-total/statistics-sewage-reservoir-total-interface"; + +import type { + MediationPumpMaintainTotalRequest, + MediationPumpStatusTotalResponse +} from "../model/statistics-total/statistics-mediation-pump-total-interface"; + +import type { + ExcretaTreatmentMaintainTotalRequest, + ExcretaTreatmentStatusTotalResponse +} from "../model/statistics-total/statistics-excreta-treatment-total-interface"; + + +import type { + PrivateSewageFacilitySummaryRequest, + PrivateSewageTab1Response, + PrivateSewageTab2Response, + PrivateSewageTab3Response +} from "../model/statistics-total/statistics-private-sewage-total-interface"; + +import type { + SewerageFinanceSummaryRequest, + SewerageFinanceSettlementResponse, + SewerageFeeResponse, + SewerageUsageFeeResponse, + SewerageOrgCauseResponse +} from "../model/statistics-total/statistics-sewerage-finance-total-interface"; + +import type { + WaterReuseSummaryRequest, + SewerageReuseStatusTab1Response, + GrayWaterStatusTab2Response, + RainwaterUtilizationStatusTab3Response, + SewageWwtRtlztnRegStatusTab4Response, + SewageWwtRtlztnSncnStatusTab5Response +} from "../model/statistics-total/statistics-sewerage-reuse-total-interface"; + +import type { + SludgeSummaryRequest, + SludgeProcessingTab1Response, + DigestionTankOperTab2Response, + SewageSludgeOperTab3Response, + HumanWasteSludgeTab4Response +} from "../model/statistics-total/statistics-sludge-total-interface"; + +import type { + SewerageIndustry, + PublicSewageStaffTab1Response, + PublicHumanWasteStaffTab2Response, + HumanWasteCollectionTab3Response, + IndividualSewageTab4Response +} from "../model/statistics-total/statistics-sewerage-industry-interface"; export class StatisticsTotalApiService { @@ -42,21 +98,6 @@ export class StatisticsTotalApiService { return response.data; } - public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise { - const response = await stClient.get ( - `${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`, - { params: params } - ); - return response.data; - } - - public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise { - const response = await stClient.get ( - `${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`, - { params: params } - ); - return response.data; - } public async getSewagePipeFactInstallStatus( params: SewagePipeTotalRequest, @@ -122,7 +163,40 @@ export class StatisticsTotalApiService { return response.data; } - /** ST00060001 —공공하수처리시설 */ + /** ★ ST00040001 — 하수저류시설 현황(합계) API */ + public async getSewerReservoirStatusTotalList( + params: SewerReservoirMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-reservoir-pump/reservoir-status`, + { params }, + ); + return response.data; + } + + /** ★ ST00040002 — 빗물펌프장 현황(합계) API */ + public async getSewerPumpStationStatusTotalList( + params: SewerReservoirMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-reservoir-pump/pump-station-status`, + { params }, + ); + return response.data; + } + + /** ST00050001 — 중계펌프장 현황(합계) API */ + public async getMediationPumpStationStatusTotalList( + params: MediationPumpMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/pump-station-status`, + { params }, + ); + return response.data; + } + /** + * ST00060001 —공공하수처리시설 */ public async getSewagePublicFacillityTotal( params: SewagePipeMaintainTotalRequest, ): Promise { @@ -132,6 +206,229 @@ export class StatisticsTotalApiService { ); return response.data; } -} + + + /** ST00070001 — 분뇨처리시설 현황(합계) API */ + public async getExcretaTreatmentStatusTotalList( + params: ExcretaTreatmentMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/excreta-treatment-status`, + { params }, + ); + return response.data; + } + + /** ST00080001 — 개인하수처리시설 > 처리용량별 오수처리시설(Tab1) */ + public async getPrivateSewageTab1List( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-sevage`, + { params }, + ); + return response.data; + } + + /** ST00080002 — 개인하수처리시설 > 정화조현황(Tab2) */ + public async getPrivateSewageTab2List( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-tank-status`, + { params }, + ); + return response.data; + } + + /** ST00080003 — 개인하수처리시설 > 개인하수도 관리지역 지정공고 현황(Tab3) */ + public async getPrivatePrivateMgmtAreaList( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-mgmt-area`, + { params }, + ); + return response.data; + } + + /** ST00090001 — 하수도 재정결산 조회(Tab1) */ + public async getSewerageFinancialList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-financial`, { params }); + return response.data; + } + + /** ST00090002 — 하수도 요금 조회(Tab2) */ + public async getSewerageFeeList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-fee`, { params }); + return response.data; + } + + /** ST00090003 — 하수도 사용료 조회(Tab3) */ + public async getSewerageUsageList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-usage`, { params }); + return response.data; + } + + /** ST00090004 — 원인자 부담금 조회(Tab4) */ + public async getSewerageOrgCauseList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-org-cause`, { params }); + return response.data; + } + + /** ST00100001 — 물재이용 > 공공하수처리수 재이용 현황 조회(Tab1) */ + public async getSewerageReuseTab1List( + params: WaterReuseSummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/pbl-swage-ruswt`, + { params }, + ); + return response.data; + } + /** + * ST00100002 — 물재이용 > TAB2 중수도 현황 조회 + */ + async getGrayWaterStatusTab2List( + params: WaterReuseSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/gray-water-status`, + { params }, + ); + return response.data; + + } + /** ★ ST00100003 — 물재이용 > TAB3 빗물이용시설현황 조회 */ + async getRainwaterStatusTab3List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/water-reuse`, { params }); + return response.data; + } + + /** ★ ST00100004 — 물재이용 > TAB4 하·폐수처리수 재이용시설 시공업 등록현황 조회 */ + async getWwtRtlztnRegStatusTab4List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/wwt-rtlztn-reg`, { params }); + return response.data; + } + + /** ★ ST00100005 — 물재이용 > TAB5 하·폐수처리수 재이용시설 시공업 인가현황 조회 */ + async getWwtRtlztnSncnStatusTab5List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/wwt-rtlztn-aprv`, { params }); + return response.data; + } + + /** + * ST00110001 — 하수및분뇨찌꺼기 > TAB1 찌꺼기처리시설현황 조회 + */ + async getSludgeProcessingTab1List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-processing`, + { params }, + ); + return response.data; + } + /** + * ST00110002 — 하수및분뇨찌꺼기 > TAB2 소화조 운영현황 조회 + */ + async getSludgeTab2List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-digestion-tank`, + { params }, + ); + return response.data; + } + + /** + * ST00110003 — 하수및분뇨찌꺼기 > TAB3 하수찌꺼기 처리시설 운영현황 조회 + */ + async getSludgeTab3List( + params: SludgeSummaryRequest + ): Promise { + // 제네릭을 추가하여 unknown 에러를 해결합니다. + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-facility-oper`, + { params } + ); + return response.data; + } + + /** + * ST00110004 — 하수및분뇨찌꺼기 > TAB4 분뇨찌꺼기(슬러지)처리현황 + */ + async getSludgeTab4List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-human-waste`, + { params }, + ); + return response.data; + } + + /** ST00120001[TAB1] 공공기관 하수분야 조회 */ + async getPublicSewageStaffTab1List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/public-sewage-staff`, + { params } + ); + return response.data; + } + /** ST00120002[TAB2] 공공기관 분뇨분야 조회 */ + async getPublicHumanWasteStaffTab2List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/public-human-waste-staff`, + { params } + ); + return response.data; + } + + /** [TAB3] 분뇨수집·운반업 조회 */ + async getHumanWasteCollectionTab3List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/human-waste-collection`, + { params } + ); + return response.data; + } + + /** [TAB4] 개인하수처리관련업 조회 */ + async getIndividualSewageTab4List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/indv-swtrmt-rel-bsns`, + { params } + ); + return response.data; + } + + /** [TAB1] 하수도서비스수준 - 민원 조회 */ + public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`, + { params: params } + ); + return response.data; + } + /** [TAB1] 하수도서비스수준 - 주민친화시설 조회 */ + public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`, + { params: params } + ); + return response.data; + } + } + export const statisticsTotalStatusApi = new StatisticsTotalApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts index 49dd44a63..95a408ca4 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts @@ -4,14 +4,16 @@ export interface SewerageServiceLevelRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } -export interface SewerageServiceLevelComplaintTotalItem { +export interface SewageComplaintSummaryItem { statsYr: string; ctpvNm: string; - sggNm: string | null; - admdstCd: string; + sggNm: string; admdstNm: string; + admdstCd: string; rowType: string; swtrmtPpltnCnt: number; whol: number; @@ -23,24 +25,22 @@ export interface SewerageServiceLevelComplaintTotalItem { sewerCrgAmt: number; etcCnt: number; crtrPpltnCvlcptEvntcsCnt: number; - rmrk: string | null; } export interface SewerageServiceLevelComplaintResponse { status: string; message?: string; - data: SewerageServiceLevelComplaintTotalItem[]; + data: SewageComplaintSummaryItem[]; } export interface SewerageServiceLevelResidentFriendlyFactTotalItem { - statsYr: string; + statsYr: string; ctpvNm: string; sggNm: string; - admdstCd: string; admdstNm: string; + admdstCd: string; rowType: string; swtrmtFcltNm: string; - fcltCpct: number; tmplntArea: number; whol: number; brkFcltArea: number; @@ -51,7 +51,6 @@ export interface SewerageServiceLevelResidentFriendlyFactTotalItem { eduMultiFcltArea: number; wtlndMakeArea: number; etcCnt: number; - rmrk: string; } export interface SewerageServiceLevelResidentFriendlyFactResponse { From 3c4a6efeeabd43b1f05dc31de9cf487eba11f1e6 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 01:01:51 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:10.=EB=AC=BC=EC=9E=AC=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SewerageWwtRtlztnSncnTab5Column.ts | 25 +++ ...atistics-sewerage-reuse-total-interface.ts | 150 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts new file mode 100644 index 000000000..148bc1927 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts @@ -0,0 +1,25 @@ +export const SewerageWwtRtlztnSncnTab5ColumnConfig = { + tabKey: "ST00100005", + tabLabel: "하·폐수처리수재이용시설사업인가현황", + rowStartNum: 2, // 행정구역 2단 레이아웃에 최적화 + fixedColumns: 1, + 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 }, + + // 사업자 정보 필드 매핑 + { name: "bplcNm", fieldName: "bplcNm", header: { text: "상호(명칭)" }, editable: false }, + { name: "rprsvNm", fieldName: "rprsvNm", header: { text: "대표자" }, editable: false }, + { name: "bplcPstnCn", fieldName: "bplcPstnCn", header: { text: "주소" }, editable: false }, + { name: "coRegYmd", fieldName: "coRegYmd", header: { text: "인가일\n(년/월/일)" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "coRegYmd" + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts new file mode 100644 index 000000000..6daaa773c --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts @@ -0,0 +1,150 @@ +/** 10. 물재이용 검색 조건 공통 파라미터 DTO */ +export interface WaterReuseSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 공공하수처리수 재이용 목록 항목 인터페이스 */ +export interface SewerageReuseStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + // 하수처리장 + swtrmtFcltNm: string; + sncnYmd: string; + prcsFcltCpct: string; + prcsQnt: number; + lglYn: string; + // 재이용시설 설치시 + instlYn: string; + oprtngStrtYmd: string; + instlAprvYmd: string; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + // 하수처리수 재이용현황 - 장내용수 + wholUseQnt: number; + ithlWshwtrQnt: number; + ithlClnQty: number; + ithlFrzgQty: number; + ithlLdhcUswtrQnt: number; + ithlDltnUswtrQnt: number; + ithlRuswtrQnt: number; + ithlEtcQnt: number; + ithlUnsdQnt: number; + // 하수처리수 재이용현황 - 장외용수 + otcntrUswtrWhol: number; + otcntrClnRstrmQnt: number; + otcntrWshwtrQnt: number; + otcntrLdhcUswtrQnt: number; + otcntrHdrplUswtrQnt: number; + otcntrRvrKeepQnt: number; + agrcltUswtrPstnQnt: number; + ugwtrChargQnt: number; + mdsrUswtrQnt: number; + // 기타 + rtlztnRt: number; +} + +export interface SewerageReuseStatusTab1Response { + status: string; + message?: string; + data: SewerageReuseStatusTotalItem[]; +} + +// ============================================================================ +// ★ [TAB2] 중수도 현황 명세 (추가 취합 완료) +// ============================================================================ +export interface GrayWaterStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + fcltPstnCn: string; + bldgSeCn: string; + lglTrgtYn: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string; + ruswtrOperAmt: number; +} + +export interface GrayWaterStatusTab2Response { + status: string; + message?: string; + data: GrayWaterStatusTotalItem[]; +} + +// ============================================================================ +// ★ [TAB3] 빗물이용시설현황 명세 (신규 추가) +// ============================================================================ +export interface RainwaterUtilizationStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + bdstTypCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + instlCmptnYmd: string; + instlAmt: number; + rcptMynCn: string; + filtrPrcsFcltYnExpln: string; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string; + anlOperAmt: number; +} +export interface RainwaterUtilizationStatusTab3Response { status: string; message?: string; data: RainwaterUtilizationStatusTotalItem[]; } + + +export interface SewageWwtRtlztnRegStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + regDt: string; +} +export interface SewageWwtRtlztnRegStatusTab4Response { status: string; message?: string; data: SewageWwtRtlztnRegStatusTotalItem[]; } + +// ============================================================================ +// ★ [TAB5] 하·폐수처리수 재이용시설 시공업 인가현황 명세 (신규 추가) +// ============================================================================ +export interface SewageWwtRtlztnSncnStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; +} +export interface SewageWwtRtlztnSncnStatusTab5Response { status: string; message?: string; data: SewageWwtRtlztnSncnStatusTotalItem[]; } \ No newline at end of file From 428b199dd37a06186282035ef74fdee2d5f2ddb3 Mon Sep 17 00:00:00 2001 From: yangsein Date: Mon, 15 Jun 2026 01:02:13 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:12=ED=95=98=EC=88=98=EB=B0=8F?= =?UTF-8?q?=EB=B6=84=EB=87=A8=EA=B4=80=EB=A6=AC=EC=82=AC=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics-sewerage-industry-interface.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts new file mode 100644 index 000000000..dd3803817 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts @@ -0,0 +1,114 @@ + +/** 12. 하수및분뇨관리사업 공통 검색 파라미터 DTO 규격 */ +export interface SewerageIndustry { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [TAB1] 공공기관 하수분야 응답 DTO */ +export interface PublicSewageStaffTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; +} + +/** [TAB1] 공공기관 하수분야 응답 DTO */ +export interface PublicSewageStaffTab1Response { + status: string; + message?: string; + data: PublicSewageStaffTotalItem[]; +} + + +/** [TAB2] 공공기관 분뇨분야 응답 DTO */ +export interface PublicHumanWasteStaffTotalItem{ + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; +} + + +export interface PublicHumanWasteStaffTab2Response { + status: string; + message?: string; + data: PublicHumanWasteStaffTotalItem[]; +} + + +/** [TAB3] 분뇨수집 .운반업 응답 DTO */ +export interface HumanWasteCollectionOperItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; +} + +export interface HumanWasteCollectionTab3Response { + status: string; + message?: string; + data: HumanWasteCollectionOperItem[]; +} + +export interface IndividualSewageRelBusinessItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; +} + +export interface IndividualSewageTab4Response { + status: string; + message?: string; + data: IndividualSewageRelBusinessItem[]; +} \ No newline at end of file From e3eb542e59e6a7aee7123dd10ec1522eaac9fb1d Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 15 Jun 2026 10:38:33 +0900 Subject: [PATCH 13/15] =?UTF-8?q?[st]=20-=20=EC=9E=85=EB=A0=A5=EB=8B=A8=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 4 +- apps/st/src/components/module/DataGrid.tsx | 160 +- .../module/SidoSggFacilityNameSearchForm.tsx | 11 +- apps/st/src/components/module/TabDataGrid.tsx | 60 +- .../VerificationResultHistoryDialog.tsx | 21 +- .../input/ExcretaTreatmentFacilityInp.tsx | 1098 +++++++++++++- .../input/PrivateSewageFacilityInp.tsx | 1285 ++++++++++++++++- .../input/PublicSewageFacilityInp.tsx | 33 +- .../statistics/input/PumpingStationInp.tsx | 1096 +++++++++++++- .../statistics/input/SewagePipeInp.tsx | 6 + .../input/SewagePipeMaintainInp.tsx | 84 +- .../input/SewageReservoirPumpInp.tsx | 596 ++++++-- .../statistics/input/SewerageFinanceInp.tsx | 796 +++++++++- .../input/SeweragePenetrationRateInp.tsx | 18 +- apps/st/src/components/type/DataGrid.type.ts | 23 +- .../column/input/ExcretaTreatFactInpColumn.ts | 145 +- .../PublicSewageFactColumn.ts | 57 +- .../column/input/PumpingStationInpColumn.ts | 128 +- .../PrivateSewageDesigStatusInpColumn.ts | 52 +- .../SepticTankStatusInpColumn.ts | 424 ++++-- .../SewageTreatFactStatusInpColumn.ts | 178 ++- .../sewerageFinance/CauserBurdenInpColumn.ts | 74 +- .../SewerageChargeInpColumn.ts | 89 +- .../SewerageFinanceSettleInpColumn.ts | 234 ++- .../SewerageUsageFeeInpColumn.ts | 159 +- .../util/grid/computedFieldCallbacks.ts | 131 ++ .../st/src/components/util/gridExcelExport.ts | 60 +- ...nput-excreta-treatment-facility-service.ts | 53 + ...s-input-private-sewage-facility-service.ts | 133 ++ ...tatistics-input-pumping-station-service.ts | 50 + ...tatistics-input-sew-supply-rate-service.ts | 5 +- ...tics-input-sewage-pipe-maintain-service.ts | 5 +- ...ics-input-sewage-reservoir-pump-service.ts | 44 +- ...atistics-input-sewerage-finance-service.ts | 68 + ...cs-public-sewage-facility-input-service.ts | 7 +- .../statistics-sewage-pipe-input-service.ts | 19 +- ...ut-excreta-treatment-facility-interface.ts | 166 +++ ...input-private-sewage-facility-interface.ts | 453 ++++++ ...tistics-input-pumping-station-interface.ts | 174 +++ ...s-input-sewage-reservoir-pump-interface.ts | 91 ++ ...istics-input-sewerage-finance-interface.ts | 277 ++++ apps/st/src/shared/api/index.ts | 1 + apps/st/src/shared/api/with-auth-header.ts | 16 + 43 files changed, 7637 insertions(+), 947 deletions(-) create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts create mode 100644 apps/st/src/shared/api/with-auth-header.ts diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index c1fdbd226..110d331c6 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -43,8 +43,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; + const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0; + const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; - if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) { + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 27fcfef82..517d26ef5 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -21,6 +21,7 @@ import type { import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { + getExcelImportColumnFieldNames, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, @@ -33,7 +34,7 @@ declare module "realgrid" { setOptions(options: { footer?: { visible?: boolean }; summaryMode?: "aggregate" | "statistical" | "none"; - checkBar?: { visible?: boolean }; + checkBar?: { visible?: boolean; exclusive?: boolean }; }): void; } } @@ -461,8 +462,10 @@ function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null { if (!dataFormat) return null; const match = dataFormat.match(/\.(0+|#+)/); const decimalToken = match?.[1]; - if (!decimalToken) return null; - return decimalToken.length; + if (decimalToken) return decimalToken.length; + // #,##0 등 정수 포맷 — 소수부 없음 → 0자리 절삭(내림) + if (/[#0]/.test(dataFormat)) return 0; + return null; } function truncateByDecimalPlaces(value: number, decimalPlaces: number): number { @@ -527,6 +530,20 @@ function buildFieldsFromColumns( "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; + const hasHalfSumOfSums = + col.computed.type === "halfSumOfSums" && + "firstSumFields" in col.computed && + "secondSumFields" in col.computed && + col.computed.firstSumFields.length > 0 && + col.computed.secondSumFields.length > 0; + const hasSumOfTwoHalfSumOfSums = + col.computed.type === "sumOfTwoHalfSumOfSums" && + "firstHalfSum" in col.computed && + "secondHalfSum" in col.computed && + col.computed.firstHalfSum.firstSumFields.length > 0 && + col.computed.firstHalfSum.secondSumFields.length > 0 && + col.computed.secondHalfSum.firstSumFields.length > 0 && + col.computed.secondHalfSum.secondSumFields.length > 0; const hasSumOfMaxGroups = col.computed.type === "sumOfMaxOfFieldAndSumGroups" && "groups" in col.computed && @@ -536,13 +553,27 @@ function buildFieldsFromColumns( col.computed.type === "pumpGrossPpqty" && "pumpGroups" in col.computed && col.computed.pumpGroups.length > 0; + const hasBodPrcsRt = + col.computed.type === "bodPrcsRt" && + "inflwField" in col.computed && + "dschgField" in col.computed; + const hasQuotientRatioPercent = + col.computed.type === "quotientRatioPercent" && + "numeratorQuotient" in col.computed && + "denominatorQuotient" in col.computed && + col.computed.numeratorQuotient.length === 2 && + col.computed.denominatorQuotient.length === 2; if ( hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || + hasHalfSumOfSums || + hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || - hasPumpGrossPpqty + hasPumpGrossPpqty || + hasBodPrcsRt || + hasQuotientRatioPercent ) { const computedCallback = getComputedValueCallback( col.computed, @@ -629,6 +660,7 @@ const DataGrid = ({ const onItemAllCheckedRef = useRef(onItemAllChecked); const isSyncingChecksFromRowDataRef = useRef(false); + const columnsFieldsSigRef = useRef(""); columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; @@ -681,20 +713,6 @@ const DataGrid = ({ .find((s) => s.menuEngNm === activeTabId)?.menuKornNm || "export"; - // 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)") - const originalHeaders: Map = new Map(); - columns.forEach((col) => { - if (col.fieldName && gridView.columnByName) { - const gridCol = gridView.columnByName(col.name); - if (gridCol && gridCol.header) { - const originalText = gridCol.header.text || ""; - originalHeaders.set(col.name, originalText); - const headerWithFieldName = `${originalText} (${col.fieldName})`; - gridCol.header.text = headerWithFieldName; - } - } - }); - // exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가) const normalizeLayoutForExport = ( layout: ColumnLayoutItem[], @@ -788,20 +806,6 @@ const DataGrid = ({ excelExportVisibilitySnapshot, ); } - // 헤더 원복 - columns.forEach((col) => { - if (col.fieldName && gridView.columnByName) { - const gridCol = gridView.columnByName(col.name); - if ( - gridCol && - gridCol.header && - originalHeaders.has(col.name) - ) { - gridCol.header.text = - originalHeaders.get(col.name) || ""; - } - } - }); // columnLayout 원복 (footerUserSpans 유지) if (normalizedLayout && !layoutRestored) { layoutRestored = true; @@ -903,36 +907,11 @@ const DataGrid = ({ : headerStartRow; const dataStartRow = maxHeaderRowIndex + 1; - // 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑 - const headerToFieldNameMap = new Map(); - headers.forEach((headerText, colIndex) => { - let fieldName: string | null = null; - - // 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)") - const fieldNameMatch = headerText.match( - /\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/, - ); - if (fieldNameMatch && fieldNameMatch[1]) { - const extractedFieldName = fieldNameMatch[1]; - if ( - columns.find( - (c) => c.fieldName === extractedFieldName, - ) - ) { - fieldName = extractedFieldName; - } - } - - // 매칭 실패 시 원본 헤더 텍스트 사용 - if (!fieldName) { - console.warn( - `엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`, - ); - fieldName = headerText; - } - - headerToFieldNameMap.set(headerText, fieldName); - }); + // 엑셀 N번째 열 → 그리드 N번째 fieldName (보내기 컬럼 순서 기준) + const excelColumnFieldNames = getExcelImportColumnFieldNames( + columns, + columnLayout, + ); // 데이터 행 읽기 const dataRows: Record[] = []; @@ -950,12 +929,8 @@ const DataGrid = ({ worksheet[ XLSX.utils.encode_cell({ r: row, c: col }) ]; - const headerName = - headers[colIndex] || `Column${colIndex + 1}`; - // 헤더 텍스트를 fieldName으로 변환 - const fieldName = - headerToFieldNameMap.get(headerName) || - headerName; + const fieldName = excelColumnFieldNames[colIndex]; + if (!fieldName) continue; let cellValue: string | number = ""; if ( @@ -1039,6 +1014,21 @@ const DataGrid = ({ const dataProvider = gridRef.current.dataProvider; applyDefaultGridEditOptions(gridView as GridViewWithEditOptions); + const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001"); + if (columnsFieldsSig !== columnsFieldsSigRef.current) { + columnsFieldsSigRef.current = columnsFieldsSig; + const dpForFields = dataProvider as { + setFields?: (f: unknown[]) => void; + setRows?: (r: Record[]) => void; + }; + try { + dpForFields.setFields?.(buildFieldsFromColumns(columns)); + dpForFields.setRows?.(rowDataRef.current); + } catch (e) { + console.warn("setFields sync:", e); + } + } + // mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리 const hasMergeCells = columns.some((col) => col.mergeCells === true); @@ -1105,6 +1095,11 @@ const DataGrid = ({ tooltip: resolveColumnTooltip(column), }), ); + gv.setColumnProperty?.( + column.name, + "editable", + column.editable !== false, + ); applyColumnListEditorProperties(gv, column); }); if (columns.some((c) => isDropDownEditorColumn(c))) { @@ -1152,7 +1147,7 @@ const DataGrid = ({ // 헤더 체크박스 표시 여부 설정 gridView.setOptions({ - checkBar: { visible: showHeaderCheckbox }, + checkBar: { visible: showHeaderCheckbox, exclusive: false }, rowIndicator: { visible: false }, }); @@ -1758,6 +1753,22 @@ const DataGrid = ({ layout={columnLayout} rows={rowData} onRGDataColumnsChanged={(grid) => { + const dp = gridRef.current?.dataProvider as + | { + setFields?: (f: unknown[]) => void; + setRows?: ( + r: Record[], + ) => void; + } + | undefined; + try { + dp?.setFields?.( + buildFieldsFromColumns(columnsRef.current), + ); + dp?.setRows?.(rowDataRef.current); + } catch (e) { + console.warn("onRGDataColumnsChanged setFields:", e); + } reapplyListEditorsOnGrid( grid as GridViewWithSetColumnProperty, columnsRef.current, @@ -1797,6 +1808,9 @@ const DataGrid = ({ dataProvider.setFields( buildFieldsFromColumns(columns), ); + columnsFieldsSigRef.current = columns + .map((c) => c.fieldName) + .join("\u0001"); } if (dataProvider?.setRows) dataProvider.setRows(rowData); @@ -1978,15 +1992,9 @@ const DataGrid = ({ {columns.map((column) => { const baseStyleName = column.styleName; - const columnReactKey = - column.values?.length && - isDropDownEditorColumn(column) - ? `${column.name}-dropdown-${column.values.length}` - : column.name; - return ( > => { const getSggAllListFetcher = async ( key: [string, string], ): Promise> => { - return statisticsApi.getSggAllList(key[1]); + return statisticsApi.getSggList(key[1]); }; type SidoSggFacilityNameSearchFormProps = { @@ -292,17 +292,12 @@ const SidoSggFacilityNameSearchForm = ({ return; } - const ctpvNm = String(row.ctpvNm ?? sido); - const sggNm = String( - row.sggNm ?? (gugun !== "ALL" && gugun ? gugun : ""), - ); - const params: SeweragePipeFacilityAdminAreaListRequest = { statsYr: statsYr.trim(), swtrmtFcltCd, sewerBzmnCd: initialSearchValues?.bsnmCd || '', - ctpvNm, - ...(doNameList.includes(ctpvNm) ? { sggNm } : {}), + ctpvNm: initialSidoValue || '', + ...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}), }; try { diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index e4c23eb83..cfed25cd9 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -17,6 +17,7 @@ import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { + getExcelImportColumnFieldNames, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, @@ -222,8 +223,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; + const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0; + const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; - if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) { + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } @@ -373,20 +376,6 @@ const TabDataGrid = ({ const columns = columnInfoItem.columns || []; const columnLayout = columnInfoItem.columnLayout || []; - // 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)") - const originalHeaders: Map = new Map(); - columns.forEach((col) => { - if (col.fieldName && gridView.columnByName) { - const gridCol = gridView.columnByName(col.name); - if (gridCol && gridCol.header) { - const originalText = gridCol.header.text || ''; - originalHeaders.set(col.name, originalText); - const headerWithFieldName = `${originalText} (${col.fieldName})`; - gridCol.header.text = headerWithFieldName; - } - } - }); - // exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가) const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => { return layout.map(item => { @@ -452,15 +441,6 @@ const TabDataGrid = ({ if (excelExportVisibilitySnapshot) { restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot); } - // 헤더 원복 - columns.forEach((col) => { - if (col.fieldName && gridView.columnByName) { - const gridCol = gridView.columnByName(col.name); - if (gridCol && gridCol.header && originalHeaders.has(col.name)) { - gridCol.header.text = originalHeaders.get(col.name) || ''; - } - } - }); // columnLayout 원복 (footerUserSpans 유지) if (normalizedLayout && !layoutRestored) { layoutRestored = true; @@ -546,29 +526,12 @@ const TabDataGrid = ({ const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow; const dataStartRow = maxHeaderRowIndex + 1; - // 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑 - const headerToFieldNameMap = new Map(); const columns = columnInfoItem.columns || []; - headers.forEach((headerText, colIndex) => { - let fieldName: string | null = null; - - // 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)") - const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/); - if (fieldNameMatch && fieldNameMatch[1]) { - const extractedFieldName = fieldNameMatch[1]; - if (columns.find(c => c.fieldName === extractedFieldName)) { - fieldName = extractedFieldName; - } - } - - // 매칭 실패 시 원본 헤더 텍스트 사용 - if (!fieldName) { - console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`); - fieldName = headerText; - } - - headerToFieldNameMap.set(headerText, fieldName); - }); + const columnLayout = columnInfoItem.columnLayout || []; + const excelColumnFieldNames = getExcelImportColumnFieldNames( + columns, + columnLayout, + ); // 데이터 행 읽기 const dataRows: Record[] = []; @@ -579,9 +542,8 @@ const TabDataGrid = ({ for (let colIndex = 0; colIndex < headers.length; colIndex++) { const col = range.s.c + colIndex; const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })]; - const headerName = headers[colIndex] || `Column${colIndex + 1}`; - // 헤더 텍스트를 fieldName으로 변환 - const fieldName = headerToFieldNameMap.get(headerName) || headerName; + const fieldName = excelColumnFieldNames[colIndex]; + if (!fieldName) continue; let cellValue: string | number = ''; if (cell && cell.v !== undefined && cell.v !== null) { diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index 745b53cc5..bde68dc26 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -181,6 +181,8 @@ export interface VerificationResultHistoryDialogProps { } | null; title?: string; emptyMessage?: string; + /** 예외 제출 취소 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */ + onExceptionResetSubmitted?: () => void | Promise; } export function VerificationResultHistoryDialog({ @@ -189,6 +191,7 @@ export function VerificationResultHistoryDialog({ queryContext, title = "검증 결과 내역", emptyMessage = "표시할 검증 결과가 없습니다.", + onExceptionResetSubmitted, }: VerificationResultHistoryDialogProps) { const stableQueryContext = useMemo(() => { if (!queryContext) return null; @@ -459,12 +462,28 @@ export function VerificationResultHistoryDialog({ alert("예외 제출 취소 처리되었습니다."); setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() })); await loadTabPage(tab, tabPages[tab].pageIndex); + try { + await onExceptionResetSubmitted?.(); + } catch { + alert( + "예외 제출 취소는 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.", + ); + } } catch { alert("예외 제출 취소 처리 중 오류가 발생했습니다."); } finally { setResetSubmitting(false); } - }, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]); + }, [ + activeTab, + alert, + checkedVrfcByTab, + entriesByTab, + loadTabPage, + onExceptionResetSubmitted, + stableQueryContext, + tabPages, + ]); const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => { const entries = entriesByTab[tab]; diff --git a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx index 626682536..560f67edd 100644 --- a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx @@ -1,80 +1,1078 @@ -import { useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type RefObject, +} from "react"; + +import { flushSync } from "react-dom"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { ValueType } from "realgrid"; +import type { RealGridReact } from "realgrid-react"; +import { LoadingWithAnimation } from "@common/components"; -import SearchForm from "@/components/module/SearchForm"; -import DataGrid from "@/components/module/DataGrid"; -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 AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; +import LastYearDataButton from "@/components/button/LastYearDataButton"; import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import SubmitDailog from "./SubmitDailog"; +import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn"; +import type { + CellStyleCallbackParams, + CellTooltipCallbackParams, + ColumnConfig, +} from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { formatCompactYmdForGrid, toCompactYmd } from "@/components/util/grid/ymdFormat"; +import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { + EXCRETA_TREATMENT_FACILITY_STATS_CD, + type ExcretaTreatmentFacilityItem, + type ExcretaTreatmentFacilityRequestParams, + type ExcretaTreatmentFacilitySaveItem, + type ExcretaTreatmentFacilityVerificationResult, +} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + ApiResponse, + cancelSubmitRequest, + ImrStatusResponse, + Sgg, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; -import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn" +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +type GridRowWithVerification = Record & { + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = + isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildExcretaTreatmentFacilitySearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): ExcretaTreatmentFacilityRequestParams { + const fcltNm = values.fctName?.trim(); + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(fcltNm ? { fcltNm } : {}), + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function mapExcretaTreatmentFacilityListToGridRows( + list: ExcretaTreatmentFacilityItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const row = toGridRows([ + { + ...item, + ctpvNm, + cmptnStrtYmd: formatCompactYmdForGrid(item.cmptnStrtYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function toApiYmd(value: string | number | Date | null | undefined): string { + return toCompactYmd(value); +} + +function resolveExcretaTreatmentFacilityAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +/** 그리드 행 → ST00070001 저장/삭제 API 항목 */ +function buildExcretaTreatmentFacilitySaveItem( + row: Record, + sggOptions: Sgg[] = [], +): ExcretaTreatmentFacilitySaveItem { + const { admdstCd, admdstNm } = resolveExcretaTreatmentFacilityAdmdst( + row, + sggOptions, + ); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + telNo: String(row.telNo ?? ""), + cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd), + oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd), + wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt), + prcsFcltCpct: toGridNumber(row.prcsFcltCpct), + cpct: toGridNumber(row.cpct), + oprtngDayCnt: toGridNumber(row.oprtngDayCnt), + prcsMthdExpln: String(row.prcsMthdExpln ?? ""), + linkTmplntCn: String(row.linkTmplntCn ?? ""), + linkPrcsQnt: toGridNumber(row.linkPrcsQnt), + inflwBodWqVl: toGridNumber(row.inflwBodWqVl), + inflwTocWqVl: toGridNumber(row.inflwTocWqVl), + inflwSsWqVl: toGridNumber(row.inflwSsWqVl), + inflwTnWqVl: toGridNumber(row.inflwTnWqVl), + inflwTpWqVl: toGridNumber(row.inflwTpWqVl), + inflwClfgCnt: toGridNumber(row.inflwClfgCnt), + dschgBodWqVl: toGridNumber(row.dschgBodWqVl), + dschgTocWqVl: toGridNumber(row.dschgTocWqVl), + dschgSsWqVl: toGridNumber(row.dschgSsWqVl), + dschgTnWqVl: toGridNumber(row.dschgTnWqVl), + dschgTpWqVl: toGridNumber(row.dschgTpWqVl), + dschgClfgCnt: toGridNumber(row.dschgClfgCnt), + wtsysNm: String(row.wtsysNm ?? ""), + trbtrNm: String(row.trbtrNm ?? ""), + msirbCn: String(row.msirbCn ?? ""), + ssirbCn: String(row.ssirbCn ?? ""), + operMthdCdCn: String(row.operMthdCdCn ?? ""), + wholNope: toGridNumber(row.wholNope), + sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function createEmptyExcretaTreatmentFacilityRow( + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + for (const col of ExcretaTreatFactInpColumn.columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} // 하수도 통계 관리 > 07. 분뇨처리시설 const ExcretaTreatmentFacilityInp = () => { - - const submitExceptionFunc = () => { - console.log('submit exception'); - // 예외제출 로직 + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + + const [rowData, setRowData] = useState[]>([]); + const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = + useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [statusInfo, setStatusInfo] = useState(null); + const [statsCd, setStatsCd] = useState(""); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState< + Record + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [deleteList, setDeleteList] = useState[]>([]); + + const gridRef = useRef(null); + const gridGetRowsRef = + useRef<(() => Record[]) | null>(null); + const sggOptionsSidoRef = useRef(null); + const [searchLoaded, setSearchLoaded] = useState(false); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const res = await statisticsApi.getColumnGuides( + EXCRETA_TREATMENT_FACILITY_STATS_CD, + ); + if (cancelled) return; + if (res.status !== "success" || !Array.isArray(res.data)) { + setColumnGuidesByEngCol({}); + return; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + setColumnGuidesByEngCol(next); + } catch { + if (!cancelled) setColumnGuidesByEngCol({}); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const columnInfo = useMemo(() => { + const enterable = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const rawColumns = ExcretaTreatFactInpColumn.columns ?? []; + const base = enterable + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + + const columns = base + .map((col: ColumnConfig) => { + if (col.fieldName === "ctpvNm") { + return { ...col, editable: false as const }; + } + if (col.fieldName === "sggNm" && enterable) { + return buildSggNmColumn(col, true, sggOptions); + } + return col; + }) + .map((col) => { + const guide = columnGuidesByEngCol[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + + return { + columns, + columnLayout: ExcretaTreatFactInpColumn.columnLayout ?? [], + }; + }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); + + const canEditGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + + const getCurrentRows = (): Record[] => + gridGetRowsRef.current?.() ?? rowData; + + const commitGrid = () => { + const gridView = gridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } }; - const getLastYearDataFunc = () => { - console.log('get last year data'); - // 지난연도 데이터 가져오기 로직 + const fetchExcretaTreatmentFacilityList = async ( + values: SearchFormValues, + overrides?: { mode?: string }, + ): Promise => { + const response = + await statisticsInputExcretaTreatmentFacilityApi.getExcretaTreatmentFacilityList( + buildExcretaTreatmentFacilitySearchParam(values, overrides), + ); + + if (response.status !== "success" || !response.data) { + alert(response.message ?? "데이터 조회 중 오류가 발생했습니다."); + setSearchLoaded(false); + return false; + } + + const { + commonInfo, + statusInfo: nextStatusInfo, + excretaTreatmentFacilityList, + } = response.data; + const list = excretaTreatmentFacilityList ?? []; + + setRowData(mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm)); + setStatusInfo(nextStatusInfo); + setStatsCd(commonInfo.statsCd); + setIsNotApplicable(false); + setSearchLoaded(true); + return true; }; - const submitFunc = () => { - console.log('submit'); - // 제출 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + setInputPeriodEnterable(isInputPeriodCurrent(periods)); + + setIsLoading(true); + setHasSearched(true); + setLastSearchedValues(values); + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + setSearchLoaded(false); + return; + } + + await fetchExcretaTreatmentFacilityList(values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + setRowData([]); + setStatusInfo(null); + setSearchLoaded(false); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setDeleteList([]); + setRowData([]); + setLastSearchedValues(null); + setHasSearched(false); + setStatusInfo(null); + setStatsCd(""); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setSearchLoaded(false); + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched || !searchLoaded) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + await fetchExcretaTreatmentFacilityList(values, { mode: "PRE" }); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const saveFunc = () => { - console.log('save'); - // 저장 로직 + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildExcretaTreatmentFacilitySaveItem(row, sggOptions), + ); + const deleteListForSave = deleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputExcretaTreatmentFacilityApi.saveExcretaTreatmentFacilityList( + { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + deleteList: deleteListForSave, + }, + ); + + if (response.status === "success") { + setDeleteList([]); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 + const submitFunc = () => { + void (async () => { + try { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + } catch { + alert("제출 중 오류가 발생했습니다."); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildExcretaTreatmentFacilitySaveItem(row, sggOptions), + ); + const deleteListForSubmit = deleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + setDeleteList([]); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = + await statisticsInputExcretaTreatmentFacilityApi.submitExcretaTreatmentFacilityList( + { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList, + deleteList: deleteListForSubmit, + }, + ); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForSubmit, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitGrid(); + const currentRows = getCurrentRows(); + const newRow = createEmptyExcretaTreatmentFacilityRow(ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowData([...currentRows, newRow]); + }); }; const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 + commitGrid(); + const currentRows = getCurrentRows(); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const gridView = gridRef.current?.gridView as + | { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; + } + | undefined; + + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? + gridView?.getCheckedItems?.(false) ?? + []; + + if (checkedIndexes.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const indexSet = new Set( + checkedIndexes.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < currentRows.length, + ), + ); + const removedRows = currentRows.filter((_, idx) => indexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + setDeleteList((prev) => [...prev, ...removedRows]); + setRowData(currentRows.filter((_, idx) => !indexSet.has(idx))); }; - - const [rowData, setRowData] = useState[]>([]); - const [isNotApplicable, setIsNotApplicable] = useState(false); + + const verificationCellStyle = ({ + fieldName, + row, + }: CellStyleCallbackParams): string | undefined => { + 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" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: CellTooltipCallbackParams): string | undefined => { + 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검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 -"; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const toolbarSnapshot = computeTabToolbarSnapshot( + statusInfo, + rowData.length > 0, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + toolbarSnapshot.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + toolbarSnapshot.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarSnapshot.showActionToolbarCard && toolbarGroupAllowed; + const showArticleToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const showNotApplicable = + searchLoaded && + hasSearched && + rowData.length === 0 && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const tabEnterableForGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); return ( <> - -
-
- - - - -
+
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {statusInfo?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showArticleToolbarCard && ( +
+
+ {showNotApplicable && ( + + )} + + +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} + } + columnInfo={columnInfo} + rowStartNum={ExcretaTreatFactInpColumn.rowStartNum} + fixedColumns={ExcretaTreatFactInpColumn.fixedColumns} + rowData={rowData} + setRowData={setRowData} + getGridDataRef={gridGetRowsRef} + showHeaderCheckbox + statusInfo={statusInfo} + isExcelImport={ + searchLoaded && tabEnterableForGrid && gugunOk + } + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + />
-
-
- - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchExcretaTreatmentFacilityList(values); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchExcretaTreatmentFacilityList(values); + }} + /> + + ); }; -export default ExcretaTreatmentFacilityInp; \ No newline at end of file +export default ExcretaTreatmentFacilityInp; diff --git a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx index b430d6584..de789139c 100644 --- a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx @@ -1,62 +1,1283 @@ -import { useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/api/input/statistics-input-private-sewage-facility-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + PrivateSewageDesigStatusSaveItem, + PrivateSewageFacilityVerificationResult, + PrivateSewageDesigStatusItem, + PrivateSewageDesigStatusSaveRequest, + PrivateSewageDesigStatusSubmitRequest, + SepticTankStatusSaveItem, + SepticTankStatusSaveRequest, + SepticTankStatusSubmitRequest, + SewageTreatFactStatusItem, + SewageTreatFactStatusSaveItem, + SewageTreatFactStatusSaveRequest, + SewageTreatFactStatusSubmitRequest, + SepticTankStatusItem, + SewageTreatFactStatusRequestParams, +} from "@/entities/statistics/model/input/statistics-input-private-sewage-facility-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import SubmitDailog from "./SubmitDailog"; + +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn"; +import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn"; +import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { useAdminStore } from "@/store/admin-store"; -import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn" -import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn" -import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn" +const PrivateSewageFacilityInpTabList = [ + SewageTreatFactStatusInpColumn, + SepticTankStatusInpColumn, + PrivateSewageDesigStatusInpColumn, +] as const; + +const firstTabKey = SewageTreatFactStatusInpColumn.tabKey; + +/** getColumnGuides API는 statsCd 기준 — 3번째 탭 tabKey와 statsCd가 다름 */ +const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record = { + [SewageTreatFactStatusInpColumn.tabKey]: SewageTreatFactStatusInpColumn.tabKey, + [SepticTankStatusInpColumn.tabKey]: SepticTankStatusInpColumn.tabKey, + [PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003", +}; + +const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from( + new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)), +); + +type GridRowWithVerification = Record & { + verificationResult?: PrivateSewageFacilityVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof PrivateSewageFacilityInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function buildPrivateSewageFacilitySearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewageTreatFactStatusRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function tabRowDataExists( + m: Map[]>, + tabKey: string, +): boolean { + const rows = m.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +function mapRegionalListToGridRows< + T extends { verificationResult?: PrivateSewageFacilityVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber( + columns: { fieldName?: string; dataFormat?: string }[], +) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (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); + }; +} + +function buildSewageTreatFactStatusSaveList( + currentData: Record[], +): SewageTreatFactStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"), + step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"), + step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"), + step4StaaPrcsQnt: formatNumber(d.step4StaaPrcsQnt, "step4StaaPrcsQnt"), + step5StaaPrcsQnt: formatNumber(d.step5StaaPrcsQnt, "step5StaaPrcsQnt"), + step6StaaPrcsQnt: formatNumber(d.step6StaaPrcsQnt, "step6StaaPrcsQnt"), + step7StaaPrcsQnt: formatNumber(d.step7StaaPrcsQnt, "step7StaaPrcsQnt"), + step1StaaExcpPrcsQnt: formatNumber(d.step1StaaExcpPrcsQnt, "step1StaaExcpPrcsQnt"), + step2StaaExcpPrcsQnt: formatNumber(d.step2StaaExcpPrcsQnt, "step2StaaExcpPrcsQnt"), + step3StaaExcpPrcsQnt: formatNumber(d.step3StaaExcpPrcsQnt, "step3StaaExcpPrcsQnt"), + step4StaaExcpPrcsQnt: formatNumber(d.step4StaaExcpPrcsQnt, "step4StaaExcpPrcsQnt"), + step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"), + step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"), + step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +function buildSepticTankStatusSaveList( + currentData: Record[], +): SepticTankStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"), + step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"), + staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"), + staaInsdAertMthdCnt: formatNumber(d.staaInsdAertMthdCnt, "staaInsdAertMthdCnt"), + staaInsdCntcAertCnt: formatNumber(d.staaInsdCntcAertCnt, "staaInsdCntcAertCnt"), + staaInsdEtcCnt: formatNumber(d.staaInsdEtcCnt, "staaInsdEtcCnt"), + step1StaaInsdNope: formatNumber(d.step1StaaInsdNope, "step1StaaInsdNope"), + step2StaaInsdNope: formatNumber(d.step2StaaInsdNope, "step2StaaInsdNope"), + step3StaaInsdNope: formatNumber(d.step3StaaInsdNope, "step3StaaInsdNope"), + step4StaaInsdNope: formatNumber(d.step4StaaInsdNope, "step4StaaInsdNope"), + step5StaaInsdNope: formatNumber(d.step5StaaInsdNope, "step5StaaInsdNope"), + step6StaaInsdNope: formatNumber(d.step6StaaInsdNope, "step6StaaInsdNope"), + step7StaaInsdNope: formatNumber(d.step7StaaInsdNope, "step7StaaInsdNope"), + step8StaaInsdNope: formatNumber(d.step8StaaInsdNope, "step8StaaInsdNope"), + staaExcpSttkNprsflCnt: formatNumber(d.staaExcpSttkNprsflCnt, "staaExcpSttkNprsflCnt"), + step1StaaExcpSttkFrcCnt: formatNumber(d.step1StaaExcpSttkFrcCnt, "step1StaaExcpSttkFrcCnt"), + staaExcpSprktpFltrtbCnt: formatNumber(d.staaExcpSprktpFltrtbCnt, "staaExcpSprktpFltrtbCnt"), + staaExcpAertMthdCnt: formatNumber(d.staaExcpAertMthdCnt, "staaExcpAertMthdCnt"), + staaExcpCntcAertCnt: formatNumber(d.staaExcpCntcAertCnt, "staaExcpCntcAertCnt"), + staaExcpEtcCnt: formatNumber(d.staaExcpEtcCnt, "staaExcpEtcCnt"), + step1StaaExcpNope: formatNumber(d.step1StaaExcpNope, "step1StaaExcpNope"), + step2StaaExcpNope: formatNumber(d.step2StaaExcpNope, "step2StaaExcpNope"), + step3StaaExcpNope: formatNumber(d.step3StaaExcpNope, "step3StaaExcpNope"), + step4StaaExcpNope: formatNumber(d.step4StaaExcpNope, "step4StaaExcpNope"), + step5StaaExcpNope: formatNumber(d.step5StaaExcpNope, "step5StaaExcpNope"), + step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"), + step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"), + step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +function buildPrivateSewageDesigStatusSaveList( + currentData: Record[], +): PrivateSewageDesigStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"), + indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"), + spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"), + spctnkQnt: formatNumber(d.spctnkQnt, "spctnkQnt"), + drtwPrcsFcltGrssFcltCnt: formatNumber( + d.drtwPrcsFcltGrssFcltCnt, + "drtwPrcsFcltGrssFcltCnt", + ), + drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function resolvePrivateSewageFacilityTab(tabKey: string) { + return { + isSewageTreatTab: tabKey === SewageTreatFactStatusInpColumn.tabKey, + isSepticTankTab: tabKey === SepticTankStatusInpColumn.tabKey, + isDesigStatusTab: tabKey === PrivateSewageDesigStatusInpColumn.tabKey, + }; +} // 하수도 통계 관리 > 08. 개인하수처리시설 const PrivateSewageFacilityInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const globalProgressVisible = useGlobalProgressStore((state) => state.visible); - // tab list - const PrivateSewageFacilityInpTabList = [ - SewageTreatFactStatusInpColumn, - SepticTankStatusInpColumn, - PrivateSewageDesigStatusInpColumn - ]; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | 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; + }); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewageTreatFactStatusRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + if (tabKey === SewageTreatFactStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getSewageTreatFactStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewageTreatFactStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewageTreatFactStatusList ?? []) as SewageTreatFactStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewageTreatFactStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SepticTankStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getSepticTankStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, septicTankStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (septicTankStatusList ?? []) as SepticTankStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SepticTankStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === PrivateSewageDesigStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getPrivateSewageDesigStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, privateSewageDesigStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (privateSewageDesigStatusList ?? []) as PrivateSewageDesigStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PrivateSewageDesigStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildPrivateSewageFacilitySearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + const latestRowsInPrevTab = getTabGridRows(prevTabKey); + if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(prevTabKey, latestRowsInPrevTab); + return next; + }); + } + + activeTabRef.current = tabKey; + setActiveTab(tabKey); + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildPrivateSewageFacilitySearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {}; + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + false, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean } }; + + if (isSewageTreatTab) { + const param: SewageTreatFactStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewageTreatFactStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus( + param, + ); + } else if (isSepticTankTab) { + const param: SepticTankStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSepticTankStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param); + } else { + const param: PrivateSewageDesigStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildPrivateSewageDesigStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus( + param, + ); + } + + if (response.status !== "success") { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("저장할 수 없는 탭입니다."); + return; + } + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (!(await confirm("저장 하시겠습니까?"))) return; + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("저장할 데이터가 없습니다."); + return; + } + + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (isSewageTreatTab) { + const param: SewageTreatFactStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewageTreatFactStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.saveSewageTreatFactStatus( + param, + ); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isSepticTankTab) { + const param: SepticTankStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSepticTankStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.saveSepticTankStatus(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + const param: PrivateSewageDesigStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildPrivateSewageDesigStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.savePrivateSewageDesigStatus( + param, + ); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of PrivateSewageFacilityInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = + activeTabToolbar; + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + 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" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + 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검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {isLoading && !globalProgressVisible && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {PrivateSewageFacilityInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {PrivateSewageFacilityInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + 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; + }); + }} + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + false, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default PrivateSewageFacilityInp; \ No newline at end of file +export default PrivateSewageFacilityInp; diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 30162faac..d0c73759e 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -210,7 +210,7 @@ const PublicSewageFacilityInp = () => { const currentData = st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) { - alert('제출할 데이터가 없습니다.'); + alert('해당사항 없음을 체크해주세요.'); return; } const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); @@ -291,15 +291,15 @@ const PublicSewageFacilityInp = () => { const currentData = st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; - if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) { - alert('제출할 데이터가 없습니다.'); + const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); + if (!treatAsNoApplicable && submitList.length === 0) { + alert('해당사항 없음을 체크해주세요.'); return; } try { showGlobalProgress({ label: '서버에 요청 중...', progress: 0 }); const userInfo = await getUserInfo(); - const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab; const requestData = { @@ -530,19 +530,19 @@ const PublicSewageFacilityInp = () => { if (!(await confirm('저장 하시겠습니까?'))) return; if (activeTab === 'ST00060001') { + const noApplicable = getNoApplicableForRequest(); + const treatAsNoApplicable = noApplicable || isNotApplicable; + const currentData = + st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; + const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); + if (!treatAsNoApplicable && saveList.length === 0) { + alert('해당사항 없음을 체크해주세요.'); + return; + } + try { showGlobalProgress({ label: '저장 진행 중...', progress: 0 }); const userInfo = await getUserInfo(); - const noApplicable = getNoApplicableForRequest(); - const treatAsNoApplicable = noApplicable || isNotApplicable; - const currentData = - st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; - if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) { - alert('저장할 데이터가 없습니다.'); - return; - } - - const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); const param: PublicSewageFacilitySaveRequest = { statsYr: lastSearchedValues.year ?? '', ctpvNm: lastSearchedValues.sido ?? '', @@ -1309,6 +1309,11 @@ const PublicSewageFacilityInp = () => { open={verificationHistoryOpen} onOpenChange={setVerificationHistoryOpen} queryContext={verificationQueryContext} + onExceptionResetSubmitted={async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx || !searchParamRef.current) return; + await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx); + }} /> 05. 펌프장 +type GridRowWithVerification = Record & { + verificationResult?: PumpingStationVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = + isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildPumpingStationSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): PumpingStationRequestParams { + const fcltNm = values.fctName?.trim(); + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(fcltNm ? { fcltNm } : {}), + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function mapPumpingStationListToGridRows( + list: PumpingStationItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const row = toGridRows([ + { + ...item, + ctpvNm, + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function resolvePumpingStationAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +/** 그리드 행 → ST00050001 저장/삭제 API 항목 */ +function buildPumpingStationSaveItem( + row: Record, + sggOptions: Sgg[] = [], +): PumpingStationSaveItem { + const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions); + const fcltSn = String(row.fcltSn ?? "").trim(); + return { + admdstCd, + admdstNm, + fcltSn: fcltSn, + swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""), + fcltNm: String(row.fcltNm ?? ""), + pumpSeNm: String(row.pumpSeNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd), + planMltvArea: toGridNumber(row.planMltvArea), + sndyArea: toGridNumber(row.sndyArea), + rainArea: toGridNumber(row.rainArea), + drtwPumpN1DmtrArea: toGridNumber(row.drtwPumpN1DmtrArea), + drtwPumpN1Ppqty: toGridNumber(row.drtwPumpN1Ppqty), + drtwPumpN1Cnt: toGridNumber(row.drtwPumpN1Cnt), + drtwPumpN1RsrvCnt: toGridNumber(row.drtwPumpN1RsrvCnt), + drtwPumpN2DmtrArea: toGridNumber(row.drtwPumpN2DmtrArea), + drtwPumpN2Ppqty: toGridNumber(row.drtwPumpN2Ppqty), + drtwPumpN2Cnt: toGridNumber(row.drtwPumpN2Cnt), + drtwPumpN2RsrvCnt: toGridNumber(row.drtwPumpN2RsrvCnt), + drtwPumpN3DmtrArea: toGridNumber(row.drtwPumpN3DmtrArea), + drtwPumpN3Ppqty: toGridNumber(row.drtwPumpN3Ppqty), + drtwPumpN3Cnt: toGridNumber(row.drtwPumpN3Cnt), + drtwPumpN3RsrvCnt: toGridNumber(row.drtwPumpN3RsrvCnt), + exlnPumpN1DmtrArea: toGridNumber(row.exlnPumpN1DmtrArea), + exlnPumpN1Ppqty: toGridNumber(row.exlnPumpN1Ppqty), + exlnPumpN1Cnt: toGridNumber(row.exlnPumpN1Cnt), + exlnPumpN1RsrvCnt: toGridNumber(row.exlnPumpN1RsrvCnt), + exlnPumpN2DmtrArea: toGridNumber(row.exlnPumpN2DmtrArea), + exlnPumpN2Ppqty: toGridNumber(row.exlnPumpN2Ppqty), + exlnPumpN2Cnt: toGridNumber(row.exlnPumpN2Cnt), + exlnPumpN2RsrvCnt: toGridNumber(row.exlnPumpN2RsrvCnt), + exlnPumpN3DmtrArea: toGridNumber(row.exlnPumpN3DmtrArea), + exlnPumpN3Ppqty: toGridNumber(row.exlnPumpN3Ppqty), + exlnPumpN3Cnt: toGridNumber(row.exlnPumpN3Cnt), + exlnPumpN3RsrvCnt: toGridNumber(row.exlnPumpN3RsrvCnt), + anlElpwrUseQnt: toGridNumber(row.anlElpwrUseQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function createEmptyPumpingStationRow( + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + for (const col of PumpingStationInpColumn.columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +// 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴) const PumpingStationInp = () => { - - const getLastYearDataFunc = () => { - console.log('get last year data'); - // 지난연도 데이터 가져오기 로직 - }; - - const submitFunc = () => { - console.log('submit'); - // 제출 로직 - }; - - const saveFunc = () => { - console.log('save'); - // 저장 로직 - }; - - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 - }; - - const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 - }; + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const [rowData, setRowData] = useState[]>([]); const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = + useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [statusInfo, setStatusInfo] = useState(null); + const [statsCd, setStatsCd] = useState(""); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState< + Record + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [pumpingStationDeleteList, setPumpingStationDeleteList] = useState< + Record[] + >([]); + + const gridRef = useRef(null); + const gridGetRowsRef = + useRef<(() => Record[]) | null>(null); + const sggOptionsSidoRef = useRef(null); + const [searchLoaded, setSearchLoaded] = useState(false); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const res = await statisticsApi.getColumnGuides(PUMPING_STATION_STATS_CD); + if (cancelled) return; + if (res.status !== "success" || !Array.isArray(res.data)) { + setColumnGuidesByEngCol({}); + return; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + setColumnGuidesByEngCol(next); + } catch { + if (!cancelled) setColumnGuidesByEngCol({}); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const columnInfo = useMemo(() => { + const enterable = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const rawColumns = PumpingStationInpColumn.columns ?? []; + const base = enterable + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + + const columns = base + .map((col: ColumnConfig) => { + if (col.fieldName === "ctpvNm") { + return { ...col, editable: false as const }; + } + if (col.fieldName === "sggNm" && enterable) { + return buildSggNmColumn(col, true, sggOptions); + } + return col; + }) + .map((col) => { + const guide = columnGuidesByEngCol[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + + return { + columns, + columnLayout: PumpingStationInpColumn.columnLayout ?? [], + }; + }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); + + const canEditGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + + const getCurrentRows = (): Record[] => + gridGetRowsRef.current?.() ?? rowData; + + const commitGrid = () => { + const gridView = gridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const fetchPumpingStationList = async ( + values: SearchFormValues, + overrides?: { mode?: string }, + ): Promise => { + const response = await statisticsInputPumpingStationApi.getPumpingStationList( + buildPumpingStationSearchParam(values, overrides), + ); + + if (response.status !== "success" || !response.data) { + alert(response.message ?? "데이터 조회 중 오류가 발생했습니다."); + setSearchLoaded(false); + return false; + } + + const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } = + response.data; + const list = pumpingStationList ?? []; + + setRowData(mapPumpingStationListToGridRows(list, commonInfo.ctpvNm)); + setStatusInfo(nextStatusInfo); + setStatsCd(commonInfo.statsCd); + setIsNotApplicable(false); + setSearchLoaded(true); + return true; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + setInputPeriodEnterable(isInputPeriodCurrent(periods)); + + setIsLoading(true); + setHasSearched(true); + setLastSearchedValues(values); + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + setSearchLoaded(false); + return; + } + + await fetchPumpingStationList(values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + setRowData([]); + setStatusInfo(null); + setSearchLoaded(false); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setPumpingStationDeleteList([]); + setRowData([]); + setLastSearchedValues(null); + setHasSearched(false); + setStatusInfo(null); + setStatsCd(""); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setSearchLoaded(false); + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched || !searchLoaded) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + await fetchPumpingStationList(values, { mode: "PRE" }); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCd || PUMPING_STATION_STATS_CD, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await fetchPumpingStationList(lastSearchedValues); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const submitFunc = () => { + void (async () => { + try { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + } catch { + alert("제출 중 오류가 발생했습니다."); + } + })(); + }; + + const saveFunc = () => { + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildPumpingStationSaveItem(row, sggOptions), + ); + const deleteList = pumpingStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildPumpingStationSaveItem(row, sggOptions)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = await statisticsInputPumpingStationApi.savePumpingStationList({ + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + deleteList, + }); + + if (response.status === "success") { + setPumpingStationDeleteList([]); + await fetchPumpingStationList(lastSearchedValues); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const submitList = noApplicable + ? [] + : currentData.map((row) => buildPumpingStationSaveItem(row, sggOptions)); + const deleteList = pumpingStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildPumpingStationSaveItem(row, sggOptions)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const statsCdForSubmit = statsCd || PUMPING_STATION_STATS_CD; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + setPumpingStationDeleteList([]); + await fetchPumpingStationList(lastSearchedValues); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = await statisticsInputPumpingStationApi.submitPumpingStationList({ + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList, + deleteList, + }); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForSubmit, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitGrid(); + const currentRows = getCurrentRows(); + const newRow = createEmptyPumpingStationRow(ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowData([...currentRows, newRow]); + }); + }; + + const deleteArticleFunc = () => { + commitGrid(); + const currentRows = getCurrentRows(); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const gridView = gridRef.current?.gridView as + | { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; + } + | undefined; + + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? + gridView?.getCheckedItems?.(false) ?? + []; + + if (checkedIndexes.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const indexSet = new Set( + checkedIndexes.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < currentRows.length, + ), + ); + const removedRows = currentRows.filter((_, idx) => indexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + setPumpingStationDeleteList((prev) => [...prev, ...removedRows]); + setRowData(currentRows.filter((_, idx) => !indexSet.has(idx))); + }; + + const verificationCellStyle = ({ + fieldName, + row, + }: CellStyleCallbackParams): string | undefined => { + 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" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: CellTooltipCallbackParams): string | undefined => { + 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검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 -"; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const toolbarSnapshot = computeTabToolbarSnapshot( + statusInfo, + rowData.length > 0, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + toolbarSnapshot.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + toolbarSnapshot.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarSnapshot.showActionToolbarCard && toolbarGroupAllowed; + const showArticleToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const showNotApplicable = + searchLoaded && + hasSearched && + rowData.length === 0 && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const tabEnterableForGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); return ( <> - -
-
- - - -
+
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {statusInfo?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showArticleToolbarCard && ( +
+
+ {showNotApplicable && ( + + )} + + +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} + } + columnInfo={columnInfo} + rowStartNum={PumpingStationInpColumn.rowStartNum} + fixedColumns={PumpingStationInpColumn.fixedColumns} + rowData={rowData} + setRowData={setRowData} + getGridDataRef={gridGetRowsRef} + showHeaderCheckbox + statusInfo={statusInfo} + isExcelImport={ + searchLoaded && tabEnterableForGrid && gugunOk + } + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + />
-
-
- - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchPumpingStationList(values); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchPumpingStationList(values); + }} + /> + + ); }; -export default PumpingStationInp; \ No newline at end of file +export default PumpingStationInp; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 6d33e5388..37468b59d 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -2119,6 +2119,12 @@ const SewagePipeInp = () => { } : null } + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.year || !values?.sido || !values?.bsnmCd) return; + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + }} /> tab.tabKey).filter(Boolean), + ), +); + const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({ columns: tab.columns ?? [], columnLayout: tab.columnLayout ?? [], @@ -302,6 +308,9 @@ const SewagePipeMaintainInp = () => { const [rowDataMap, setRowDataMap] = useState< Map[]> >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); @@ -328,6 +337,37 @@ const SewagePipeMaintainInp = () => { } }, [activeTabId, inputFocusNonce]); + useEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return; + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => { + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [tabKey, next] as const; + } catch { + return [tabKey, {}] as const; + } + }), + ); + if (cancelled) return; + setColumnGuidesByTab(Object.fromEntries(entries)); + })(); + return () => { + cancelled = true; + }; + }, [activeTabId]); + const commitToolbarSnapshotForLoadedTab = ( tabKey: string, statusInfo: SewerageStatusInfo | null, @@ -951,22 +991,22 @@ const SewagePipeMaintainInp = () => { tabStatusInfoMap.get(tabKey) ?? null, inputPeriodEnterable, ) || isAdminInfo; - const columns = (tab.columns ?? []).map((col: ColumnConfig) => - canEdit - ? col - : { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }, - ); + const rawColumns = tab.columns ?? []; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base + .map((col) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); map.set(tabKey, { columns, columnLayout: tab.columnLayout ?? [], }); } return map; - }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo]); + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); const searchContextForToolbar = lastSearchedValues ?? searchValues; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; @@ -1120,7 +1160,7 @@ const SewagePipeMaintainInp = () => { return ( { open={verificationHistoryOpen} onOpenChange={setVerificationHistoryOpen} queryContext={verificationQueryContext} + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> { open={verificationResultDialogOpen} onOpenChange={setVerificationResultDialogOpen} queryContext={verificationQueryContext} + onExceptionSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); diff --git a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx index b2ffb84e4..bbca7cf5c 100644 --- a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx +++ b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx @@ -1,5 +1,6 @@ import { useCallback, + useEffect, useMemo, useRef, useState, @@ -25,8 +26,11 @@ import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid, { applyDropDownColumnToGrid } from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import SubmitDailog from "./SubmitDailog"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { CellStyleCallbackParams, @@ -45,22 +49,27 @@ import { toCompactYmd, } from "@/components/util/grid/ymdFormat"; import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { SEWAGE_RESERVOIR_STATS_CD, type SewageReservoirItem, type SewageReservoirRequestParams, type SewageReservoirSaveItem, type RainfallPumpStationItem, + type RainfallPumpStationSaveItem, type SewageReservoirVerificationResult, } from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import type { ApiResponse, cancelSubmitRequest, + ImrStatusResponse, Sgg, SewerageStatusInfo, } from "@/entities/statistics/model/statistics-interface"; -import { useAlertStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { useAdminStore } from "@/store/admin-store"; @@ -80,6 +89,12 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR const rainfallPumpTabKey = RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp"; +const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set( + SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean), + ), +); + type GridRowWithVerification = Record & { verificationResult?: SewageReservoirVerificationResult; }; @@ -210,11 +225,7 @@ function buildSggNmColumn( sggOptions: Sgg[], ): ColumnConfig { if (!canEdit) { - return { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }; + return { ...col, editable: false as const }; } if (sggOptions.length === 0) { return { ...col, editable: true }; @@ -272,31 +283,7 @@ function toGridNumber(value: string | number | null | undefined): number { return Number.isFinite(n) ? n : 0; } -/** API 조회 항목 → 저장/삭제 API 형식 (조회 직후 스냅샷용) */ -function buildSewageReservoirSaveItemFromApiItem( - item: SewageReservoirItem, -): SewageReservoirSaveItem { - return buildSewageReservoirSaveItem( - item as unknown as Record, - ); -} - -function buildSewageReservoirDeleteList( - loadedSnapshot: SewageReservoirSaveItem[], - currentRows: Record[], -): SewageReservoirSaveItem[] { - const currentFcltSnSet = new Set( - currentRows - .map((row) => String(row.fcltSn ?? "").trim()) - .filter((sn) => sn !== ""), - ); - return loadedSnapshot.filter((item) => { - const sn = String(item.fcltSn ?? "").trim(); - return sn !== "" && !currentFcltSnSet.has(sn); - }); -} - -/** 시군구명(`sggNm`) 기준 행정구역 코드·명 */ +/** 시군구명(`sggNm`) 기준 `admdstCd`(bsnmCd)·`admdstNm`(시군구명) */ function resolveSewageReservoirAdmdst( sggNm: string, sggOptions: Sgg[], @@ -304,7 +291,7 @@ function resolveSewageReservoirAdmdst( const trimmed = sggNm.trim(); const matched = sggOptions.find((s) => s.gugun === trimmed); if (matched) { - return { admdstCd: matched.adstDstrCd, admdstNm: matched.gugun }; + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } return { admdstCd: "", admdstNm: trimmed }; } @@ -318,10 +305,12 @@ function buildSewageReservoirSaveItem( String(row.sggNm ?? ""), sggOptions, ); + const fcltSn = String(row.fcltSn ?? "").trim(); return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? ""), + // 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1" + fcltSn: fcltSn, fcltNm: String(row.fcltNm ?? ""), fcltPstnCn: String(row.fcltPstnCn ?? ""), oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd), @@ -345,9 +334,9 @@ function buildSewageReservoirSaveItem( pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), - prcsgFcltYn: String(row.prcsgFcltYn ?? ""), - ddrztnEqpmYn: String(row.ddrztnEqpmYn ?? ""), - oslfEtgtYn: String(row.oslfEtgtYn ?? ""), + prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), mltvMthCn: nullableTrimmedString(row.mltvMthCn), impdMthCn: nullableTrimmedString(row.impdMthCn), impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn), @@ -362,6 +351,56 @@ function buildSewageReservoirSaveItem( }; } +/** 그리드 행 → ST00040002 저장/삭제 API 항목 */ +function buildRainfallPumpStationSaveItem( + row: Record, + sggOptions: Sgg[] = [], +): RainfallPumpStationSaveItem { + const { admdstCd, admdstNm } = resolveSewageReservoirAdmdst( + String(row.sggNm ?? ""), + sggOptions, + ); + const fcltSn = String(row.fcltSn ?? "").trim(); + return { + admdstCd, + admdstNm, + fcltSn: fcltSn, + fcltNm: String(row.fcltNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + fcltyKndCn: String(row.fcltyKndCn ?? ""), + oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd), + planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), + wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), + rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""), + rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt), + abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt), + grssPpqty: toGridNumber(row.grssPpqty), + pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""), + pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty), + pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt), + pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt), + pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""), + pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty), + pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt), + pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt), + pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""), + pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), + pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), + pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), + prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), + prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), + ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), + prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), + ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), + oprtngDayCnt: toGridNumber(row.oprtngDayCnt), + grssMltvQnt: toGridNumber(row.grssMltvQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + function mapRainfallPumpStationListToGridRows( list: RainfallPumpStationItem[], ctpvNm: string, @@ -386,20 +425,72 @@ function mapRainfallPumpStationListToGridRows( }); } +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + // 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장 const SewageReservoirPumpInp = () => { const { setMessage: alert } = useAlertStore((state) => state); const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const submitFunc = () => { - console.log("submit"); + void (async () => { + const tabKey = activeTabRef.current; + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getCurrentRowsForTab(tabKey); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); }; - /** 마지막 조회 시 서버에 있던 하수저류시설 목록 (deleteList 비교 기준) */ - const loadedSewageReservoirSnapshotRef = useRef([]); - const cnacelSubmitFunc = () => { void (async () => { try { @@ -530,24 +621,35 @@ const SewageReservoirPumpInp = () => { .filter((idx) => idx >= 0), ); + const removedRows = currentRows.filter((_, idx) => + checkedIndexSet.has(idx), + ); const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); - if (nextRows.length === currentRows.length) { + if (removedRows.length === 0) { alert("삭제할 항목을 선택해주세요."); return; } + if (tabKey === SEWAGE_RESERVOIR_STATS_CD) { + setSewageReservoirDeleteList((prev) => [...prev, ...removedRows]); + } else if (tabKey === rainfallPumpTabKey) { + setRainfallPumpStationDeleteList((prev) => [...prev, ...removedRows]); + } + setRowDataMap((prev) => { const next = new Map(prev); next.set(tabKey, nextRows); return next; }); - }; const saveFunc = () => { void (async () => { const tabKey = activeTabRef.current; - if (tabKey !== SEWAGE_RESERVOIR_STATS_CD) { + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { alert("저장할 수 없는 탭입니다."); return; } @@ -564,41 +666,63 @@ const SewageReservoirPumpInp = () => { commitGridForTab(tabKey); const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const currentData = getCurrentRowsForTab(tabKey); + const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; const saveList = noApplicable ? [] - : currentData.map((row) => - buildSewageReservoirSaveItem(row, sggOptions), - ); - const deleteList = buildSewageReservoirDeleteList( - loadedSewageReservoirSnapshotRef.current, - currentData, - ); - - if (!noApplicable && saveList.length === 0 && deleteList.length === 0) { - alert("저장할 변경 사항이 없습니다."); + : isReservoirTab + ? currentData.map((row) => + buildSewageReservoirSaveItem(row, sggOptions), + ) + : currentData.map((row) => + buildRainfallPumpStationSaveItem(row, sggOptions), + ); + const deleteList = isReservoirTab + ? sewageReservoirDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : rainfallPumpStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); return; } + const requestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + ...(noApplicable ? { noApplicable: true } : {}), + }; + try { setIsLoading(true); const userInfo = await getUserInfo(); - const response = - await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({ - statsYr: lastSearchedValues.year ?? "", - ctpvNm: lastSearchedValues.sido ?? "", - sggNm: - lastSearchedValues.gugun === "ALL" - ? "" - : (lastSearchedValues.gugun ?? ""), - sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + const response = isReservoirTab + ? await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({ + ...requestBase, userId: userInfo.userUnqId, - ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), - ...(noApplicable ? { noApplicable: true } : {}), - saveList, - deleteList, + saveList: saveList as SewageReservoirSaveItem[], + deleteList: deleteList as SewageReservoirSaveItem[], + }) + : await statisticsInputSewageReservoirPumpApi.saveRainfallPumpStation({ + ...requestBase, + userId: userInfo.userUnqId, + saveList: saveList as RainfallPumpStationSaveItem[], + deleteList: deleteList as RainfallPumpStationSaveItem[], }); if (response.status === "success") { + if (isReservoirTab) { + setSewageReservoirDeleteList([]); + } else { + setRainfallPumpStationDeleteList([]); + } await loadActiveTabData(tabKey, lastSearchedValues, false, false); alert("저장되었습니다."); } else { @@ -612,6 +736,180 @@ const SewageReservoirPumpInp = () => { })(); }; + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGridForTab(tabKey); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getCurrentRowsForTab(tabKey); + const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; + const submitList = noApplicable + ? [] + : isReservoirTab + ? currentData.map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : currentData.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + const deleteList = isReservoirTab + ? sewageReservoirDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : rainfallPumpStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + if (isReservoirTab) { + setSewageReservoirDeleteList([]); + } else { + setRainfallPumpStationDeleteList([]); + } + await loadActiveTabData(tabKey, lastSearchedValues, false, false); + }; + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(noApplicable ? { noApplicable: true } : {}), + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = isReservoirTab + ? await statisticsInputSewageReservoirPumpApi.submitSewageReservoir({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as SewageReservoirSaveItem[], + deleteList: deleteList as SewageReservoirSaveItem[], + }) + : await statisticsInputSewageReservoirPumpApi.submitRainfallPumpStation({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as RainfallPumpStationSaveItem[], + deleteList: deleteList as RainfallPumpStationSaveItem[], + }); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const [sewageReservoirDeleteList, setSewageReservoirDeleteList] = useState< + Record[] + >([]); + const [rainfallPumpStationDeleteList, setRainfallPumpStationDeleteList] = + useState[]>([]); const [activeTab, setActiveTab] = useState(firstTabKey); const [rowDataMap, setRowDataMap] = useState< Map[]> @@ -634,7 +932,13 @@ const SewageReservoirPumpInp = () => { >(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); const reservoirGridRef = useRef(null); const rainfallPumpGridRef = useRef(null); const reservoirGridGetRowsRef = @@ -643,6 +947,36 @@ const SewageReservoirPumpInp = () => { useRef<(() => Record[]) | null>(null); const sggOptionsSidoRef = useRef(null); + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => { + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [tabKey, next] as const; + } catch { + return [tabKey, {}] as const; + } + }), + ); + if (cancelled) return; + setColumnGuidesByTab(Object.fromEntries(entries)); + })(); + return () => { + cancelled = true; + }; + }, []); + const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); const activeTabRef = useRef(activeTab); @@ -732,9 +1066,7 @@ const SewageReservoirPumpInp = () => { const { commonInfo, statusInfo, sewageReservoirList } = response.data; const list = sewageReservoirList ?? []; - loadedSewageReservoirSnapshotRef.current = list - .filter((item) => String(item.fcltSn ?? "").trim() !== "") - .map(buildSewageReservoirSaveItemFromApiItem); + setSewageReservoirDeleteList([]); const gridRows = mapSewageReservoirListToGridRows( list, commonInfo.ctpvNm, @@ -771,6 +1103,7 @@ const SewageReservoirPumpInp = () => { const { commonInfo, statusInfo, rainfallPumpStationList } = response.data; const list = rainfallPumpStationList ?? []; + setRainfallPumpStationDeleteList([]); const gridRows = mapRainfallPumpStationListToGridRows( list, commonInfo.ctpvNm, @@ -837,7 +1170,8 @@ const SewageReservoirPumpInp = () => { }, []); const handleReset = () => { - loadedSewageReservoirSnapshotRef.current = []; + setSewageReservoirDeleteList([]); + setRainfallPumpStationDeleteList([]); setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); @@ -860,6 +1194,25 @@ const SewageReservoirPumpInp = () => { return; } + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + } catch { + alert("입력 기간 확인 중 오류가 발생했습니다."); + return; + } + const yearChangedFromLastSearch = lastSearchedValues != null && (lastSearchedValues.year ?? "") !== (values.year ?? ""); @@ -957,12 +1310,13 @@ const SewageReservoirPumpInp = () => { baseShowEnterableToolbarButtons && toolbarGroupAllowed; const showActionToolbarCard = baseShowActionToolbarCard && toolbarGroupAllowed; - // 항목추가/제거 버튼은 "데이터가 있든 없든", "과거 데이터(입력 불가 상태)여도" - // 조회를 하고나면 항상 보여야 합니다. const isReservoirOrRainfallTab = activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey; const showArticleToolbarCard = - hasSearched && isReservoirOrRainfallTab && toolbarGroupAllowed; + hasSearched && + isReservoirOrRainfallTab && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; const showNotApplicableForActiveTab = loadedTabKeysRef.current.has(activeTab) && @@ -1012,20 +1366,6 @@ const SewageReservoirPumpInp = () => { const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; - if (isRainfallPumpTab) { - const newRow = createEmptyRainfallPumpStationRow(ctx.sido, sggNm); - setRowDataMap((prev) => { - const next = new Map(prev); - const current = next.get(rainfallPumpTabKey) ?? []; - next.set(rainfallPumpTabKey, [...current, newRow]); - return next; - }); - - const statusInfo = tabStatusInfoMap.get(rainfallPumpTabKey) ?? null; - commitToolbarSnapshotForLoadedTab(rainfallPumpTabKey, statusInfo, true, ctx); - return; - } - let list = sggOptions; if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { list = await loadSggOptions(ctx.sido); @@ -1036,24 +1376,23 @@ const SewageReservoirPumpInp = () => { return; } - const tabKey = SEWAGE_RESERVOIR_STATS_CD; - const labels = list.map((s) => s.gugun).filter(Boolean); - const newRow = createEmptySewageReservoirRow(ctx.sido, sggNm); + const tabKey = activeTabKey; + const newRow = isReservoirTab + ? createEmptySewageReservoirRow(ctx.sido, sggNm) + : createEmptyRainfallPumpStationRow(ctx.sido, sggNm); + + commitGridForTab(tabKey); + const currentRows = getCurrentRowsForTab(tabKey); flushSync(() => { setSggOptions(list); setRowDataMap((prev) => { const next = new Map(prev); - const current = next.get(tabKey) ?? []; - next.set(tabKey, [...current, newRow]); + next.set(tabKey, [...currentRows, newRow]); return next; }); }); - // 드롭다운 컬럼은 columnInfoByTab(buildSggNmColumn) 렌더링으로 반영한다. - // 행 추가 직후 setColumnProperty를 강제 재적용하면 타이밍 이슈로 - // `fieldIndex is out of bounds`가 발생할 수 있어 제거한다. - const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; @@ -1068,24 +1407,29 @@ const SewageReservoirPumpInp = () => { inputPeriodEnterable, ) || isAdminInfo; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; - const columns = (tab.columns ?? []).map((col: ColumnConfig) => { - if (isReservoirTab && col.fieldName === "ctpvNm") { - return { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }; + const isRainfallPumpTab = tabKey === rainfallPumpTabKey; + const rawColumns = tab.columns ?? []; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col: ColumnConfig) => { + if ( + (isReservoirTab || isRainfallPumpTab) && + col.fieldName === "ctpvNm" + ) { + return { ...col, editable: false as const }; } - if (isReservoirTab && col.fieldName === "sggNm") { - return buildSggNmColumn(col, canEdit, sggOptions); + if ( + (isReservoirTab || isRainfallPumpTab) && + col.fieldName === "sggNm" && + canEdit + ) { + return buildSggNmColumn(col, true, sggOptions); } - return canEdit - ? col - : { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }; + return col; + }).map((col) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; }); map.set(tabKey, { columns, @@ -1093,7 +1437,7 @@ const SewageReservoirPumpInp = () => { }); } return map; - }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions]); + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByTab]); const verificationCellStyle = ({ fieldName, @@ -1220,7 +1564,7 @@ const SewageReservoirPumpInp = () => { return ( { showHeaderCheckbox statusInfo={tabStatusInfo} isExcelImport={ - tabRowDataExists(rowDataMap, tabKey) && + loadedTabKeysRef.current.has(tabKey) && tabEnterableForGrid && gugunOk } @@ -1281,6 +1625,28 @@ const SewageReservoirPumpInp = () => { open={verificationHistoryOpen} onOpenChange={setVerificationHistoryOpen} queryContext={verificationQueryContext} + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await loadActiveTabData(activeTabRef.current, values, false, false); + }} + /> + + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await loadActiveTabData(activeTabRef.current, values, false, false); + }} /> diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index 3f3d57f4e..55f1a9998 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -1,34 +1,539 @@ -import { useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/input/statistics-input-sewerage-finance-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + CauserBurdenItem, + SewerageChargeItem, + SewerageFinanceRequestParams, + SewerageFinanceSettleItem, + SewerageFinanceVerificationResult, + SewerageUsageFeeItem, +} from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn"; +import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn"; +import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn"; +import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { useAdminStore } from "@/store/admin-store"; -import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn" -import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn" -import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn" -import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn" +const SewerageFinanceInpTabList = [ + SewerageFinanceSettleInpColumn, + SewerageChargeInpColumn, + SewerageUsageFeeInpColumn, + CauserBurdenInpColumn, +] as const; + +const firstTabKey = SewerageFinanceSettleInpColumn.tabKey; + +const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)), +); + +type GridRowWithVerification = Record & { + verificationResult?: SewerageFinanceVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof SewerageFinanceInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function buildSewerageFinanceSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewerageFinanceRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function tabRowDataExists( + m: Map[]>, + tabKey: string, +): boolean { + const rows = m.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +function mapRegionalListToGridRows< + T extends { verificationResult?: SewerageFinanceVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} // 하수도 통계 관리 > 09. 하수도재정 const SewerageFinanceInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); - // tab list - const SewerageFinanceInpTabList = [ - SewerageFinanceSettleInpColumn, - SewerageChargeInpColumn, - SewerageUsageFeeInpColumn, - CauserBurdenInpColumn - ]; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => { + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [tabKey, next] as const; + } catch { + return [tabKey, {}] as const; + } + }), + ); + if (cancelled) return; + setColumnGuidesByTab(Object.fromEntries(entries)); + })(); + return () => { + cancelled = true; + }; + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | 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; + }); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewerageFinanceRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + if (tabKey === SewerageFinanceSettleInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageFinanceSettleList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageFinanceSettleList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageFinanceSettleList ?? []) as SewerageFinanceSettleItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageFinanceSettleInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SewerageChargeInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageChargeList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageChargeList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageChargeList ?? []) as SewerageChargeItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageChargeInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SewerageUsageFeeInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageUsageFeeList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageUsageFeeList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageUsageFeeList ?? []) as SewerageUsageFeeItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageUsageFeeInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === CauserBurdenInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getCauserBurdenList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, causerBurdenList } = response.data; + const gridRows = mapRegionalListToGridRows( + (causerBurdenList ?? []) as CauserBurdenItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(CauserBurdenInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewerageFinanceSearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + const latestRowsInPrevTab = getTabGridRows(prevTabKey); + if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(prevTabKey, latestRowsInPrevTab); + return next; + }); + } + + activeTabRef.current = tabKey; + setActiveTab(tabKey); + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildSewerageFinanceSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const submitFunc = () => { @@ -39,26 +544,249 @@ const SewerageFinanceInp = () => { // 저장 로직 }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewerageFinanceInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = + activeTabToolbar; + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + 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" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + 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검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewerageFinanceInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewerageFinanceInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + 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; + }); + }} + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default SewerageFinanceInp; \ No newline at end of file +export default SewerageFinanceInp; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 6a31e84da..50435dbb7 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -288,7 +288,8 @@ const SeweragePenetrationRateInp = () => { } else { alert(response.message); } - } catch { + } catch (error) { + console.log(error); alert('데이터 조회 중 오류가 발생했습니다.'); setRowData([]); setStatusInfo(null); @@ -790,11 +791,13 @@ const SeweragePenetrationRateInp = () => {
)} - {isLoading && !globalProgressVisible &&
-
- + {isLoading && !globalProgressVisible && +
+
+ +
-
} + } { } : null } + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await handleSearch(values); + }} /> ; type: "pumpGrossPpqty"; + } + | { + inflwField: string; + dschgField: string; + type: "bodPrcsRt"; + } + | { + /** (quotient[0]/quotient[1]) / (denominatorQuotient[0]/denominatorQuotient[1]) × 100 */ + numeratorQuotient: [string, string]; + denominatorQuotient: [string, string]; + type: "quotientRatioPercent"; }; export type ColumnConfig = { diff --git a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts index 4896a2194..ac73c825c 100644 --- a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts +++ b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts @@ -8,58 +8,83 @@ import { ValueType } from "realgrid"; export const ExcretaTreatFactInpColumn = { columns: [ // 행정구역 - { name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 기본 정보 - { name: 'ecscs_nm', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'locplcs', fieldName: '소재지', header: { text: '소재지' } }, - { name: 'telno', fieldName: '전화번호', header: { text: '전화번호' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' } }, + { name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' } }, // 날짜 정보 - { name: 'comp_dt', fieldName: '준공일 yyyy-mm-dd', header: { text: '준공일 yyyy-mm-dd' } }, - { name: 'rn_dt', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일 yyyy-mm-dd' } }, + { + name: 'cmptnStrtYmd', + fieldName: 'cmptnStrtYmd', + header: { text: '준공일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, + { + name: 'oprtngStrtYmd', + fieldName: 'oprtngStrtYmd', + header: { text: '가동개시일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 사업비/용량 정보 - { name: 'wct', fieldName: '사업비(백만원)', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ecscs_cpcty', fieldName: '시설용량(m³/일)', header: { text: '시설용량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'process_cpcty', fieldName: '처리량(m³/일)', header: { text: '처리량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'opr_daycnt', fieldName: '가동일수(일/년)', header: { text: '가동일수(일/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wholPjtcoAmt', fieldName: 'wholPjtcoAmt', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'cpct', fieldName: 'cpct', header: { text: '처리량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수(일/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 처리 공법 및 연계 - { name: 'trt_msrc', fieldName: '처리공법', header: { text: '처리공법' } }, - { name: 'cntc_prcplt', fieldName: '연계처리장명', header: { text: '연계처리장명' } }, - { name: 'assct_trt_cpc', fieldName: '연계처리량(m³/일)', header: { text: '연계처리량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리공법' } }, + { name: 'linkTmplntCn', fieldName: 'linkTmplntCn', header: { text: '연계처리장명' } }, + { name: 'linkPrcsQnt', fieldName: 'linkPrcsQnt', header: { text: '연계처리량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 유입수질 - { name: 'infl_wqlt_bod', fieldName: 'BOD', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'infl_wqlt_toc', fieldName: 'TOC', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'infl_wqlt_ss', fieldName: 'SS', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'infl_wqlt_tn', fieldName: 'T-N', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'infl_wqlt_tp', fieldName: 'T-P', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'infl_wqlt_clfgrpcnt', fieldName: '대장균군수', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'inflwBodWqVl', fieldName: 'inflwBodWqVl', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'inflwTocWqVl', fieldName: 'inflwTocWqVl', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'inflwSsWqVl', fieldName: 'inflwSsWqVl', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'inflwTnWqVl', fieldName: 'inflwTnWqVl', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwTpWqVl', fieldName: 'inflwTpWqVl', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwClfgCnt', fieldName: 'inflwClfgCnt', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 방류수질 - { name: 'effl_wqlt_bod', fieldName: 'BOD', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'effl_wqlt_toc', fieldName: 'TOC', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'effl_wqlt_ss', fieldName: 'SS', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'effl_wqlt_tn', fieldName: 'T-N', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'effl_wqlt_tp', fieldName: 'T-P', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'effl_wqlt_clfgrpcnt', fieldName: '대장균군수', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'dschgBodWqVl', fieldName: 'dschgBodWqVl', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dschgTocWqVl', fieldName: 'dschgTocWqVl', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dschgSsWqVl', fieldName: 'dschgSsWqVl', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dschgTnWqVl', fieldName: 'dschgTnWqVl', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgTpWqVl', fieldName: 'dschgTpWqVl', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgClfgCnt', fieldName: 'dschgClfgCnt', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 방류수역 - { name: 'rvbsn', fieldName: '수계', header: { text: '수계' } }, - { name: 'bstrm', fieldName: '지류', header: { text: '지류' } }, - { name: 'lgbsn', fieldName: '중권역', header: { text: '중권역' } }, - { name: 'mdmbsn', fieldName: '소권역', header: { text: '소권역' } }, + { name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' } }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } }, + { name: 'msirbCn', fieldName: 'msirbCn', header: { text: '중권역' } }, + { name: 'ssirbCn', fieldName: 'ssirbCn', header: { text: '소권역' } }, // 운영 정보 - { name: 'oper_mth', fieldName: '운영주체(자체/공기업/민간위탁)', header: { text: '운영주체(자체/공기업/민간위탁)' } }, - { name: 'cntc_prcplt_cpc', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'safe_acdnt_co', fieldName: '안전사고건수(건/인/년)', header: { text: '안전사고건수(건/인/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'operMthdCdCn', fieldName: 'operMthdCdCn', header: { text: '운영주체(자체/공기업/민간위탁)' } }, + { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수(건/인/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'rmk', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + + // Hidden Columns : 시스템 관리를 위한 값 + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설일련번호' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ @@ -67,41 +92,51 @@ export const ExcretaTreatFactInpColumn = { name: 'adminGroup', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['sido', 'gugun'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'ecscs_nm', - 'locplcs', - 'telno', - 'comp_dt', - 'rn_dt', - 'wct', - 'ecscs_cpcty', - 'process_cpcty', - 'opr_daycnt', - 'trt_msrc', - 'cntc_prcplt', - 'assct_trt_cpc', + 'fcltNm', + 'fcltPstnCn', + 'telNo', + 'cmptnStrtYmd', + 'oprtngStrtYmd', + 'wholPjtcoAmt', + 'prcsFcltCpct', + 'cpct', + 'oprtngDayCnt', + 'prcsMthdExpln', + 'linkTmplntCn', + 'linkPrcsQnt', { name: 'influenceGroup', hideChildHeaders: false, header: { text: '유입수질(mg/L, 개/mL)' }, - items: ['infl_wqlt_bod', 'infl_wqlt_toc', 'infl_wqlt_ss', 'infl_wqlt_tn', 'infl_wqlt_tp', 'infl_wqlt_clfgrpcnt'], + items: ['inflwBodWqVl', 'inflwTocWqVl', 'inflwSsWqVl', 'inflwTnWqVl', 'inflwTpWqVl', 'inflwClfgCnt'], }, { name: 'effluentGroup', hideChildHeaders: false, header: { text: '방류수질(mg/L, 개/mL)' }, - items: ['effl_wqlt_bod', 'effl_wqlt_toc', 'effl_wqlt_ss', 'effl_wqlt_tn', 'effl_wqlt_tp', 'effl_wqlt_clfgrpcnt'], + items: ['dschgBodWqVl', 'dschgTocWqVl', 'dschgSsWqVl', 'dschgTnWqVl', 'dschgTpWqVl', 'dschgClfgCnt'], }, { name: 'dischargeGroup', hideChildHeaders: false, header: { text: '방류수역' }, - items: ['rvbsn', 'bstrm', 'lgbsn', 'mdmbsn'], + items: ['wtsysNm', 'trbtrNm', 'msirbCn', 'ssirbCn'], }, - 'oper_mth', - 'cntc_prcplt_cpc', - 'safe_acdnt_co', - 'rmk', - ] + 'operMthdCdCn', + 'wholNope', + 'sftyAcdntEvntcsCnt', + 'rmrk', + 'admdstCd', + 'fcltSn', + 'admdstNm' + ], + + rowStartNum: 2, + fixedColumns: 2, + tabKey: 'ST00070001', }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts index db0c550ff..d9943bbd6 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -8,7 +8,7 @@ import { footerDecimal1, footerDecimal3,footerInt } from "@/components/util/grid export const PublicSewageFactColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' }, // 시설명 @@ -18,7 +18,20 @@ export const PublicSewageFactColumn = { { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false, styleName: 'rg-cell-readonly' }, // 시설용량 - { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { + name: 'prcsFcltCpct', + fieldName: 'prcsFcltCpct', + header: { text: '시설용량(500m³/일 이상/미만)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'], + type: 'sum' as const, + }, + }, // (m³/일) { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, @@ -56,8 +69,22 @@ export const PublicSewageFactColumn = { { name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 처리특성 - { name: 'prcsRt', fieldName: 'prcsRt', header: { text: '처리효율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, - { name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { + name: 'prcsRt', + fieldName: 'prcsRt', + header: { text: '처리효율(%)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + inflwField: 'inflwBodVl', + dschgField: 'dschgBodVl', + type: 'bodPrcsRt' as const, + }, + }, + { name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, { name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' }, editable: false, styleName: 'rg-cell-readonly' }, // 연계처리량 @@ -86,7 +113,20 @@ export const PublicSewageFactColumn = { { name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질(BOD)' }, editable: false, styleName: 'rg-cell-readonly' }, // 에너지 자립률 - { name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지자립률(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { + name: 'engySrlneRt', + fieldName: 'engySrlneRt', + header: { text: '에너지자립률(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + numeratorFields: ['engyItslfPrdctnQnt'], + denominatorFields: ['anlEngyWholUseQnt'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 에너지 자체 생산량 { name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, @@ -97,7 +137,7 @@ export const PublicSewageFactColumn = { { name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체 에너지사용량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 하수처리량당 CO2배출량 - { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-total' }, + { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, // 운영주체 { name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false, styleName: 'rg-cell-readonly' }, @@ -123,7 +163,10 @@ export const PublicSewageFactColumn = { name: 'administrative', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'swtrmtFcltNm', 'fcltPstnCn', diff --git a/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts b/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts index b478b3bc9..deb9d592e 100644 --- a/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts +++ b/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts @@ -1,6 +1,33 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 펌프장 구분(`pumpSeNm`) */ +export const PUMPING_STATION_PUMP_SE_VALUES = ["합류식", "분류식"] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + /** * 하수도 통계 관리 > 05. 펌프장 * 단일 Tab @@ -14,18 +41,70 @@ export const PumpingStationInpColumn = { // 기본 정보 { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, - { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분(합류식 분류식)' } }, + dropdownColumn( + { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분(합류식 분류식)' } }, + PUMPING_STATION_PUMP_SE_VALUES, + ), { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } }, - { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동 년월일 yyyy-mm-dd' } }, + { + name: 'oprtngStrtYmd', + fieldName: 'oprtngStrtYmd', + header: { text: '가동 년월일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, { name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 계획시간 대 최대배수량 { name: 'sndyArea', fieldName: 'sndyArea', header: { text: '청천시(A)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'rainArea', fieldName: 'rainArea', header: { text: '우천시(B)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'exlnQnt', fieldName: 'exlnQnt', header: { text: '우수량(B-A)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'exlnQnt', + fieldName: 'exlnQnt', + header: { text: '우수량(B-A)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { fields: ['rainArea', 'sndyArea'], type: 'difference' as const }, + styleName: 'rg-cell-total', + }, // 배수능력 > 총 양수량 (오수) - { name: 'drtwPumpGrssPpqty', fieldName: 'drtwPumpGrssPpqty', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'drtwPumpGrssPpqty', + fieldName: 'drtwPumpGrssPpqty', + header: { text: '총양수량(m³/min)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'drtwPumpN1Cnt', + rsrvPumpCntField: 'drtwPumpN1RsrvCnt', + perPpqtyField: 'drtwPumpN1Ppqty', + }, + { + pumpCntField: 'drtwPumpN2Cnt', + rsrvPumpCntField: 'drtwPumpN2RsrvCnt', + perPpqtyField: 'drtwPumpN2Ppqty', + }, + { + pumpCntField: 'drtwPumpN3Cnt', + rsrvPumpCntField: 'drtwPumpN3RsrvCnt', + perPpqtyField: 'drtwPumpN3Ppqty', + }, + ], + }, + }, // 배수능력 > 오수펌프(1) { name: 'drtwPumpN1DmtrArea', fieldName: 'drtwPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, @@ -46,7 +125,36 @@ export const PumpingStationInpColumn = { { name: 'drtwPumpN3RsrvCnt', fieldName: 'drtwPumpN3RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 총 양수량 (우수) - { name: 'exlnPumpGrssPpqty', fieldName: 'exlnPumpGrssPpqty', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'exlnPumpGrssPpqty', + fieldName: 'exlnPumpGrssPpqty', + header: { text: '총양수량(m³/min)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'exlnPumpN1Cnt', + rsrvPumpCntField: 'exlnPumpN1RsrvCnt', + perPpqtyField: 'exlnPumpN1Ppqty', + }, + { + pumpCntField: 'exlnPumpN2Cnt', + rsrvPumpCntField: 'exlnPumpN2RsrvCnt', + perPpqtyField: 'exlnPumpN2Ppqty', + }, + { + pumpCntField: 'exlnPumpN3Cnt', + rsrvPumpCntField: 'exlnPumpN3RsrvCnt', + perPpqtyField: 'exlnPumpN3Ppqty', + }, + ], + }, + }, // 배수능력 > 우수펌프(1) { name: 'exlnPumpN1DmtrArea', fieldName: 'exlnPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, @@ -83,7 +191,10 @@ export const PumpingStationInpColumn = { name: 'administrativeArea', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'swtrmtFcltNm', 'fcltNm', @@ -147,5 +258,8 @@ export const PumpingStationInpColumn = { 'admdstCd', 'fcltSn', 'admdstNm' - ] + ], + rowStartNum: 3, + fixedColumns: 3, + tabKey: 'ST00050001', }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts index b7eaf2cca..88cdc2e40 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts @@ -5,66 +5,64 @@ import { ValueType } from "realgrid"; export const PrivateSewageDesigStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 개인하수도관리지역공고현황 - { name: 'MGMT_AR', fieldName: '공고지역(개소)', header: { text: '공고지역(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'APPN_SCOPE', fieldName: '지정범위(㎢)', header: { text: '지정범위(㎢)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'indvSwagePbancRgnCnt', fieldName: 'indvSwagePbancRgnCnt', header: { text: '공고지역(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'indvSwageDsgnScpLen', fieldName: 'indvSwageDsgnScpLen', header: { text: '지정범위(㎢)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 개인하수도관리지역공고현황 > 정화조 - { name: 'STTY_CNT', fieldName: '개소수', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_CPC', fieldName: '처리용량(인/일)', header: { text: '처리용량(인/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'spctnkGrssFcltCnt', fieldName: 'spctnkGrssFcltCnt', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'spctnkQnt', fieldName: 'spctnkQnt', header: { text: '처리용량(인/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 개인하수도관리지역공고현황 > 오수시설 - { name: 'WRRTN_CNT', fieldName: '개소수', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'WRRTN_CPC', fieldName: '처리용량(m³/일)', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drtwPrcsFcltGrssFcltCnt', fieldName: 'drtwPrcsFcltGrssFcltCnt', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPrcsFcltQnt', fieldName: 'drtwPrcsFcltQnt', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // 검증 필드 (hidden) - { name: 'CHK_MGMT_AR', fieldName: 'CHK_MGMT_AR', header: { text: '' } }, - { name: 'CHK_APPN_SCOPE', fieldName: 'CHK_APPN_SCOPE', header: { text: '' } }, - { name: 'CHK_STTY_CNT', fieldName: 'CHK_STTY_CNT', header: { text: '' } }, - { name: 'CHK_STTY_CPC', fieldName: 'CHK_STTY_CPC', header: { text: '' } }, - { name: 'CHK_WRRTN_CNT', fieldName: 'CHK_WRRTN_CNT', header: { text: '' } }, - { name: 'CHK_WRRTN_CPC', fieldName: 'CHK_WRRTN_CPC', header: { text: '' } }, - - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_notice', header: { text: '개인하수도관리지역공고현황 (하수도법제34조의2)' }, items: [ - 'MGMT_AR', - 'APPN_SCOPE', + 'indvSwagePbancRgnCnt', + 'indvSwageDsgnScpLen', { name: 'group_septic', header: { text: '정화조' }, items: [ - 'STTY_CNT', - 'STTY_CPC' + 'spctnkGrssFcltCnt', + 'spctnkQnt' ] }, { name: 'group_sewage', header: { text: '오수시설' }, items: [ - 'WRRTN_CNT', - 'WRRTN_CPC' + 'drtwPrcsFcltGrssFcltCnt', + 'drtwPrcsFcltQnt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts index 4d2b45b8f..ef1d1aa1c 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts @@ -5,182 +5,316 @@ import { ValueType } from "realgrid"; export const SepticTankStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, - // 총 시설 수 - { name: 'TOT_FCLTY_CNT', fieldName: '총 시설 수 (A+D)', header: { text: '총 시설 수 (A+D)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + // 총 시설 수 — (B+C)/2 + (E+F)/2 = A + D + { + name: 'grssFcltCnt', + fieldName: 'grssFcltCnt', + header: { text: '총 시설 수 (A+D)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstHalfSum: { + firstSumFields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + secondSumFields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + }, + secondHalfSum: { + firstSumFields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + secondSumFields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + }, + type: 'sumOfTwoHalfSumOfSums' as const, + }, + }, // 하수처리구역 내 > 총계 - { name: 'SPZI_SM', fieldName: '총계 A=(B+C)/2', header: { text: '총계 A=(B+C)/2' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdWhol', + fieldName: 'staaInsdWhol', + header: { text: '총계 A=(B+C)/2' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstSumFields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + secondSumFields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + type: 'halfSumOfSums' as const, + }, + }, // 하수처리구역 내 > 처리방식별 - { name: 'SPZI_MTHD_SM', fieldName: '계(B)', header: { text: '계(B)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdPrcsMthByWhol', + fieldName: 'staaInsdPrcsMthByWhol', + header: { text: '계(B)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + type: 'sum' as const, + }, + }, // 하수처리구역 내 > 처리방식별 > 부패탱크 - { name: 'SPZI_ROT_NPFL_CNT', fieldName: '자연유하', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_ROT_ENFCT_PUMP_1', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdSttkNprsflCnt', fieldName: 'staaInsdSttkNprsflCnt', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step1StaaInsdSttkFrcCnt', fieldName: 'step1StaaInsdSttkFrcCnt', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 처리방식별 - { name: 'SPZI_WTROT_NPFL_CNT', fieldName: '살수여상', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_TFM', fieldName: '폭기방법', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_ARTM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_CTARM', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdSprktpFltrtbCnt', fieldName: 'staaInsdSprktpFltrtbCnt', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdAertMthdCnt', fieldName: 'staaInsdAertMthdCnt', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdCntcAertCnt', fieldName: 'staaInsdCntcAertCnt', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdEtcCnt', fieldName: 'staaInsdEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 처리용량별 - { name: 'SPZI_PROC_CPC_SM', fieldName: '계(C)', header: { text: '계(C)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_1', fieldName: '10인조이하', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_8', fieldName: '1,000 이상', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdPrcsCpctByWhol', + fieldName: 'staaInsdPrcsCpctByWhol', + header: { text: '계(C)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaInsdNope', fieldName: 'step1StaaInsdNope', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaInsdNope', fieldName: 'step2StaaInsdNope', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaInsdNope', fieldName: 'step3StaaInsdNope', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaInsdNope', fieldName: 'step4StaaInsdNope', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaInsdNope', fieldName: 'step5StaaInsdNope', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaInsdNope', fieldName: 'step6StaaInsdNope', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaInsdNope', fieldName: 'step7StaaInsdNope', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step8StaaInsdNope', fieldName: 'step8StaaInsdNope', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 총계 - { name: 'SPZO_SM', fieldName: '총계 D=(E+F)/2', header: { text: '총계 D=(E+F)/2' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpWhol', + fieldName: 'staaExcpWhol', + header: { text: '총계 D=(E+F)/2' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstSumFields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + secondSumFields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + type: 'halfSumOfSums' as const, + }, + }, // 하수처리구역 외 > 처리방식별 - { name: 'SPZO_MTHD_SM', fieldName: '계(E)', header: { text: '계(E)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpPrcsMthByWhol', + fieldName: 'staaExcpPrcsMthByWhol', + header: { text: '계(E)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + type: 'sum' as const, + }, + }, // 하수처리구역 외 > 처리방식별 > 부패탱크 - { name: 'SPZO_ROT_NPFL_CNT', fieldName: '자연유하', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_ROT_ENFCT_PUMP_1', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpSttkNprsflCnt', fieldName: 'staaExcpSttkNprsflCnt', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step1StaaExcpSttkFrcCnt', fieldName: 'step1StaaExcpSttkFrcCnt', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 처리방식별 - { name: 'SPZO_WTROT_NPFL_CNT', fieldName: '살수여상', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_TFM', fieldName: '폭기방법', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_ARTM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_CTARM', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpSprktpFltrtbCnt', fieldName: 'staaExcpSprktpFltrtbCnt', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpAertMthdCnt', fieldName: 'staaExcpAertMthdCnt', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpCntcAertCnt', fieldName: 'staaExcpCntcAertCnt', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpEtcCnt', fieldName: 'staaExcpEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 처리용량별 - { name: 'SPZO_PROC_CPC_SM', fieldName: '계(F)', header: { text: '계(F)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_1', fieldName: '10인조이하', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_8', fieldName: '1,000 이상', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpPrcsCpctByWhol', + fieldName: 'staaExcpPrcsCpctByWhol', + header: { text: '계(F)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaExcpNope', fieldName: 'step1StaaExcpNope', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaExcpNope', fieldName: 'step2StaaExcpNope', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaExcpNope', fieldName: 'step3StaaExcpNope', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaExcpNope', fieldName: 'step4StaaExcpNope', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaExcpNope', fieldName: 'step5StaaExcpNope', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaExcpNope', fieldName: 'step6StaaExcpNope', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaExcpNope', fieldName: 'step7StaaExcpNope', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step8StaaExcpNope', fieldName: 'step8StaaExcpNope', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - - // 검증 필드 (검증 여부 표시용 - 빨간색 표시) - { name: 'CHK_TOT_FCLTY_CNT', fieldName: 'CHK_TOT_FCLTY_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_SM', fieldName: 'CHK_SPZI_SM', header: { text: '' } }, - { name: 'CHK_SPZI_MTHD_SM', fieldName: 'CHK_SPZI_MTHD_SM', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_NPFL_CNT', fieldName: 'CHK_SPZI_ROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_TANK_SM', fieldName: 'CHK_SPZI_ROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_NPFL_CNT', fieldName: 'CHK_SPZI_IMHF_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_TANK_SM', fieldName: 'CHK_SPZI_IMHF_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_NPFL_CNT', fieldName: 'CHK_SPZI_WTROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_TANK_SM', fieldName: 'CHK_SPZI_WTROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_TFM', fieldName: 'CHK_SPZI_TFM', header: { text: '' } }, - { name: 'CHK_SPZI_ARTM', fieldName: 'CHK_SPZI_ARTM', header: { text: '' } }, - { name: 'CHK_SPZI_CTARM', fieldName: 'CHK_SPZI_CTARM', header: { text: '' } }, - { name: 'CHK_SPZI_NPFL', fieldName: 'CHK_SPZI_NPFL', header: { text: '' } }, - { name: 'CHK_SPZI_ENFCT_PUMP', fieldName: 'CHK_SPZI_ENFCT_PUMP', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_SM', fieldName: 'CHK_SPZI_PROC_CPC_SM', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_1', fieldName: 'CHK_SPZI_PROC_CPC_1', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_2', fieldName: 'CHK_SPZI_PROC_CPC_2', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_3', fieldName: 'CHK_SPZI_PROC_CPC_3', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_4', fieldName: 'CHK_SPZI_PROC_CPC_4', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_5', fieldName: 'CHK_SPZI_PROC_CPC_5', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_6', fieldName: 'CHK_SPZI_PROC_CPC_6', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_7', fieldName: 'CHK_SPZI_PROC_CPC_7', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_8', fieldName: 'CHK_SPZI_PROC_CPC_8', header: { text: '' } }, - { name: 'CHK_SPZO_SM', fieldName: 'CHK_SPZO_SM', header: { text: '' } }, - { name: 'CHK_SPZO_MTHD_SM', fieldName: 'CHK_SPZO_MTHD_SM', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_NPFL_CNT', fieldName: 'CHK_SPZO_ROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_TANK_SM', fieldName: 'CHK_SPZO_ROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_NPFL_CNT', fieldName: 'CHK_SPZO_IMHF_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_TANK_SM', fieldName: 'CHK_SPZO_IMHF_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_NPFL_CNT', fieldName: 'CHK_SPZO_WTROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_TANK_SM', fieldName: 'CHK_SPZO_WTROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_TFM', fieldName: 'CHK_SPZO_TFM', header: { text: '' } }, - { name: 'CHK_SPZO_ARTM', fieldName: 'CHK_SPZO_ARTM', header: { text: '' } }, - { name: 'CHK_SPZO_CTARM', fieldName: 'CHK_SPZO_CTARM', header: { text: '' } }, - { name: 'CHK_SPZO_NPFL', fieldName: 'CHK_SPZO_NPFL', header: { text: '' } }, - { name: 'CHK_SPZO_ENFCT_PUMP', fieldName: 'CHK_SPZO_ENFCT_PUMP', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_SM', fieldName: 'CHK_SPZO_PROC_CPC_SM', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_1', fieldName: 'CHK_SPZO_PROC_CPC_1', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_2', fieldName: 'CHK_SPZO_PROC_CPC_2', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_3', fieldName: 'CHK_SPZO_PROC_CPC_3', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_4', fieldName: 'CHK_SPZO_PROC_CPC_4', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_5', fieldName: 'CHK_SPZO_PROC_CPC_5', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_6', fieldName: 'CHK_SPZO_PROC_CPC_6', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_7', fieldName: 'CHK_SPZO_PROC_CPC_7', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_8', fieldName: 'CHK_SPZO_PROC_CPC_8', header: { text: '' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'TOT_FCLTY_CNT', + 'grssFcltCnt', { name: 'group_spzi', header: { text: '하수처리구역 내' }, items: [ - 'SPZI_SM', + 'staaInsdWhol', { name: 'group_spzi_mthd', header: { text: '처리방식별' }, items: [ - 'SPZI_MTHD_SM', + 'staaInsdPrcsMthByWhol', { name: 'group_spzi_rot', header: { text: '부패탱크' }, items: [ - 'SPZI_ROT_NPFL_CNT', - 'SPZI_ROT_ENFCT_PUMP_1' + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt' ] }, - 'SPZI_WTROT_NPFL_CNT', - 'SPZI_TFM', - 'SPZI_ARTM', - 'SPZI_CTARM' + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt' ] }, { name: 'group_spzi_proc', header: { text: '처리용량별' }, items: [ - 'SPZI_PROC_CPC_SM', - 'SPZI_PROC_CPC_1', - 'SPZI_PROC_CPC_2', - 'SPZI_PROC_CPC_3', - 'SPZI_PROC_CPC_4', - 'SPZI_PROC_CPC_5', - 'SPZI_PROC_CPC_6', - 'SPZI_PROC_CPC_7', - 'SPZI_PROC_CPC_8' + 'staaInsdPrcsCpctByWhol', + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope' ] } ] @@ -189,47 +323,49 @@ export const SepticTankStatusInpColumn = { name: 'group_spzo', header: { text: '하수처리구역 외' }, items: [ - 'SPZO_SM', + 'staaExcpWhol', { name: 'group_spzo_mthd', header: { text: '처리방식별' }, items: [ - 'SPZO_MTHD_SM', + 'staaExcpPrcsMthByWhol', { name: 'group_spzo_rot', header: { text: '부패탱크' }, items: [ - 'SPZO_ROT_NPFL_CNT', - 'SPZO_ROT_ENFCT_PUMP_1' + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt' ] }, - 'SPZO_WTROT_NPFL_CNT', - 'SPZO_TFM', - 'SPZO_ARTM', - 'SPZO_CTARM' + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt' ] }, { name: 'group_spzo_proc', header: { text: '처리용량별' }, items: [ - 'SPZO_PROC_CPC_SM', - 'SPZO_PROC_CPC_1', - 'SPZO_PROC_CPC_2', - 'SPZO_PROC_CPC_3', - 'SPZO_PROC_CPC_4', - 'SPZO_PROC_CPC_5', - 'SPZO_PROC_CPC_6', - 'SPZO_PROC_CPC_7', - 'SPZO_PROC_CPC_8' + 'staaExcpPrcsCpctByWhol', + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 4, fixedColumns: 1, - tabKey: 'septicTankStatusInp', + tabKey: 'ST00080002', tabLabel: '정화조 현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts index 40e77978a..2c247a3de 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts @@ -5,65 +5,119 @@ import { ValueType } from "realgrid"; export const SewageTreatFactStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 총계 - { name: 'STTY_SM', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'wholDrtwPrcsFcltCnt', + fieldName: 'wholDrtwPrcsFcltCnt', + header: { text: '총계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt', + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt', + ], + type: 'sum' as const, + }, + }, // 오수처리시설 > 하수처리구역 내(m³/일) - { name: 'STTY_ZONE_INNER_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_2', fieldName: '2이하', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_3', fieldName: '3~10', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_11', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_21', fieldName: '21~50', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_51', fieldName: '51~100', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_101', fieldName: '101~500', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_501', fieldName: '501이상', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'wholStaaCnt', + fieldName: 'wholStaaCnt', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaPrcsQnt', fieldName: 'step1StaaPrcsQnt', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaPrcsQnt', fieldName: 'step2StaaPrcsQnt', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaPrcsQnt', fieldName: 'step3StaaPrcsQnt', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaPrcsQnt', fieldName: 'step4StaaPrcsQnt', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaPrcsQnt', fieldName: 'step5StaaPrcsQnt', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaPrcsQnt', fieldName: 'step6StaaPrcsQnt', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaPrcsQnt', fieldName: 'step7StaaPrcsQnt', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 오수처리시설 > 하수처리구역 외(m³/일) - { name: 'STTY_ZONE_OUTER_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_2', fieldName: '2이하', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_3', fieldName: '3~10', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_11', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_21', fieldName: '21~50', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_51', fieldName: '51~100', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_101', fieldName: '101~500', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_501', fieldName: '501이상', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpWhol', + fieldName: 'staaExcpWhol', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaExcpPrcsQnt', fieldName: 'step1StaaExcpPrcsQnt', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaExcpPrcsQnt', fieldName: 'step2StaaExcpPrcsQnt', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaExcpPrcsQnt', fieldName: 'step3StaaExcpPrcsQnt', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaExcpPrcsQnt', fieldName: 'step4StaaExcpPrcsQnt', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaExcpPrcsQnt', fieldName: 'step5StaaExcpPrcsQnt', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaExcpPrcsQnt', fieldName: 'step6StaaExcpPrcsQnt', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaExcpPrcsQnt', fieldName: 'step7StaaExcpPrcsQnt', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - - // 검증 필드 (검증 여부 표시용, 빨간색 표시) - { name: 'CHK_STTY_SM', fieldName: 'CHK_STTY_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_SM', fieldName: 'CHK_STTY_ZONE_INNER_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_2', fieldName: 'CHK_STTY_ZONE_INNER_2', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_3', fieldName: 'CHK_STTY_ZONE_INNER_3', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_11', fieldName: 'CHK_STTY_ZONE_INNER_11', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_21', fieldName: 'CHK_STTY_ZONE_INNER_21', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_51', fieldName: 'CHK_STTY_ZONE_INNER_51', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_101', fieldName: 'CHK_STTY_ZONE_INNER_101', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_501', fieldName: 'CHK_STTY_ZONE_INNER_501', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_SM', fieldName: 'CHK_STTY_ZONE_OUTER_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_2', fieldName: 'CHK_STTY_ZONE_OUTER_2', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_3', fieldName: 'CHK_STTY_ZONE_OUTER_3', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_11', fieldName: 'CHK_STTY_ZONE_OUTER_11', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_21', fieldName: 'CHK_STTY_ZONE_OUTER_21', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_51', fieldName: 'CHK_STTY_ZONE_OUTER_51', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_101', fieldName: 'CHK_STTY_ZONE_OUTER_101', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_501', fieldName: 'CHK_STTY_ZONE_OUTER_501', header: { text: '' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'STTY_SM', + 'wholDrtwPrcsFcltCnt', { name: 'group_stty', header: { text: '오수처리시설' }, @@ -72,36 +126,38 @@ export const SewageTreatFactStatusInpColumn = { name: 'group_inner', header: { text: '하수처리구역 내(m³/일)' }, items: [ - 'STTY_ZONE_INNER_SM', - 'STTY_ZONE_INNER_2', - 'STTY_ZONE_INNER_3', - 'STTY_ZONE_INNER_11', - 'STTY_ZONE_INNER_21', - 'STTY_ZONE_INNER_51', - 'STTY_ZONE_INNER_101', - 'STTY_ZONE_INNER_501' + 'wholStaaCnt', + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt' ] }, { name: 'group_outer', header: { text: '하수처리구역 외(m³/일)' }, items: [ - 'STTY_ZONE_OUTER_SM', - 'STTY_ZONE_OUTER_2', - 'STTY_ZONE_OUTER_3', - 'STTY_ZONE_OUTER_11', - 'STTY_ZONE_OUTER_21', - 'STTY_ZONE_OUTER_51', - 'STTY_ZONE_OUTER_101', - 'STTY_ZONE_OUTER_501' + 'staaExcpWhol', + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewageTreatFactStatusInp', + tabKey: 'ST00080001', tabLabel: '처리용량별 오수처리시설 현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts index 419f00d9d..7ae03817e 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts @@ -5,46 +5,72 @@ import { ValueType } from "realgrid"; export const CauserBurdenInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 부과액 - { name: 'LEVY_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'levyEvntcsCnt', fieldName: 'levyEvntcsCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'levyAmt', fieldName: 'levyAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대상별 징수현황 > 계 - { name: 'TRGET_LEV_STTUS_CO', fieldName: '건수', header: { text: '건수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TRGET_LEV_STTUS_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'trgtByWholClctnEvntcsCnt', + fieldName: 'trgtByWholClctnEvntcsCnt', + header: { text: '건수' }, + editable: false, + footer: footerInt, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: ['mltvEqpmEvntcsCnt', 'othrCstrnOthrActrCnt'], + type: 'sum' as const, + }, + }, + { + name: 'trgtByWholClctnAmt', + fieldName: 'trgtByWholClctnAmt', + header: { text: '금액(백만원)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['mltvEqpmAmt', 'othrCstrnOthrActrAmt'], + type: 'sum' as const, + }, + }, // 대상별 징수현황 > 배수설비설치자 - { name: 'DRNG_EQP_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'DRNG_EQP_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mltvEqpmEvntcsCnt', fieldName: 'mltvEqpmEvntcsCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mltvEqpmAmt', fieldName: 'mltvEqpmAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대상별 징수현황 > 타공사·타행위자 - { name: 'CNTRWK_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CNTRWK_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'othrCstrnOthrActrCnt', fieldName: 'othrCstrnOthrActrCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'othrCstrnOthrActrAmt', fieldName: 'othrCstrnOthrActrAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - - // 참조 필드 - { name: 'AMNTBRNCSR', fieldName: 'AMNTBRNCSR', header: { text: 'AMNTBRNCSR' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_assess', header: { text: '부과액' }, - items: ['LEVY_CO', 'LEVY_AMOUNT'] + items: ['levyEvntcsCnt', 'levyAmt'] }, { name: 'group_collection', @@ -53,25 +79,27 @@ export const CauserBurdenInpColumn = { { name: 'coll_group_total', header: { text: '계' }, - items: ['TRGET_LEV_STTUS_CO', 'TRGET_LEV_STTUS_AMOUNT'] + items: ['trgtByWholClctnEvntcsCnt', 'trgtByWholClctnAmt'] }, { name: 'coll_group_inst', header: { text: '배수설비설치자(하수도법 제61조 제1항)' }, - items: ['DRNG_EQP_CO', 'DRNG_EQP_AMOUNT'] + items: ['mltvEqpmEvntcsCnt', 'mltvEqpmAmt'] }, { name: 'coll_group_otr', header: { text: '타공사·타행위자(하수도법 제61조 제2항)' }, - items: ['CNTRWK_CO', 'CNTRWK_AMOUNT'] + items: ['othrCstrnOthrActrCnt', 'othrCstrnOthrActrAmt'] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'causerBurdenInp', + tabKey: 'ST00090004', tabLabel: '원인자 부담금' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts index 5940acc81..a86546960 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts @@ -5,45 +5,86 @@ import { ValueType } from "realgrid"; export const SewerageChargeInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 요금 - { name: 'FYER_LEVY_QY', fieldName: '연간조정량(m³)(A)', header: { text: '연간조정량(m³)(A)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '하수도사용료수익(원)(B)', header: { text: '하수도사용료수익(원)(B)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'AVRG_UNTPC', fieldName: 'm³당요금(원/m³) C=(B/A)', header: { text: 'm³당요금(원/m³) C=(B/A)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlLevyQnt', fieldName: 'anlLevyQnt', header: { text: '연간조정량(m³)(A)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'levyAmt', fieldName: 'levyAmt', header: { text: '하수도사용료수익(원)(B)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'avgUntprcAmt', + fieldName: 'avgUntprcAmt', + header: { text: 'm³당요금(원/m³) C=(B/A)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['levyAmt', 'anlLevyQnt'], + type: 'quotient' as const, + }, + }, - { name: 'SMRIZE_PRMPC', fieldName: '총괄원가(원)(D)', header: { text: '총괄원가(원)(D)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SMRIZE_UNIT_PRMPC', fieldName: 'm³당원가(원/m³) E=(D/A)', header: { text: 'm³당원가(원/m³) E=(D/A)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RLTY_RT', fieldName: '요금현실화율(%) F=(C/E*100)', header: { text: '요금현실화율(%) F=(C/E*100)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'grssPrmcoAmt', fieldName: 'grssPrmcoAmt', header: { text: '총괄원가(원)(D)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'grssUnitPrmcoAmt', + fieldName: 'grssUnitPrmcoAmt', + header: { text: 'm³당원가(원/m³) E=(D/A)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['grssPrmcoAmt', 'anlLevyQnt'], + type: 'quotient' as const, + }, + }, + { + name: 'rlztnRt', + fieldName: 'rlztnRt', + header: { text: '요금현실화율(%) F=(C/E*100)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorQuotient: ['levyAmt', 'anlLevyQnt'] as [string, string], + denominatorQuotient: ['grssPrmcoAmt', 'anlLevyQnt'] as [string, string], + type: 'quotientRatioPercent' as const, + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: '비고', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 참조 필드 - { name: 'SEWER_RNTFEE', fieldName: 'SEWER_RNTFEE', header: { text: 'SEWER_RNTFEE' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'C_LEVY_AMOUNT', fieldName: 'C_LEVY_AMOUNT', header: { text: 'C_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'FYER_LEVY_QY', - 'LEVY_AMOUNT', - 'AVRG_UNTPC', - 'SMRIZE_PRMPC', - 'SMRIZE_UNIT_PRMPC', - 'RLTY_RT', - 'RMK' + 'anlLevyQnt', + 'levyAmt', + 'avgUntprcAmt', + 'grssPrmcoAmt', + 'grssUnitPrmcoAmt', + 'rlztnRt', + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'sewerageChargeInp', + tabKey: 'ST00090002', tabLabel: '하수도 요금' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts index 24b46e1e6..5db0443df 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts @@ -5,146 +5,238 @@ import { ValueType } from "realgrid"; export const SewerageFinanceSettleInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 재정별 사업비 - { name: 'FNANC_WCT_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'NTNXPN', fieldName: '중앙정부지원액', header: { text: '중앙정부지원액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - + { + name: 'fnnByWholPjtcoAmt', + fieldName: 'fnnByWholPjtcoAmt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'cnterGovSprtAmt', + 'mctpvLcexpAmt', + 'wareaSggLclAmt', + 'sewerUseAmt', + 'orgprBrdnAmt', + 'etcIncmAmt', + 'crfwAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'cnterGovSprtAmt', fieldName: 'cnterGovSprtAmt', header: { text: '중앙정부지원액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + // 재정별 사업비 > 지방비 - { name: 'LCLEXPNS', fieldName: '특·광역시, 도', header: { text: '특·광역시, 도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'LCLTY_CT', fieldName: '시,군,구 지자체', header: { text: '시,군,구 지자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mctpvLcexpAmt', fieldName: 'mctpvLcexpAmt', header: { text: '특·광역시, 도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wareaSggLclAmt', fieldName: 'wareaSggLclAmt', header: { text: '시,군,구 지자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 재정별 사업비 - { name: 'SEWER_RNTFEE', fieldName: '하수도사용료', header: { text: '하수도사용료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'AMNTBRNCSR', fieldName: '원인자부담금', header: { text: '원인자부담금' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_INCME', fieldName: '기타잡수입', header: { text: '기타잡수입' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'CYFD_AMOUNT', fieldName: '이월액', header: { text: '이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sewerUseAmt', fieldName: 'sewerUseAmt', header: { text: '하수도사용료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'orgprBrdnAmt', fieldName: 'orgprBrdnAmt', header: { text: '원인자부담금' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcIncmAmt', fieldName: 'etcIncmAmt', header: { text: '기타잡수입' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'crfwAmt', fieldName: 'crfwAmt', header: { text: '이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 부채액 - { name: 'DEBT_AMOUNT', fieldName: '부채액(백만원)', header: { text: '부채액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'lbltAmt', fieldName: 'lbltAmt', header: { text: '부채액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 계 - { name: 'PRPOS_WCT_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'usgByWholPjtcoAmt', + fieldName: 'usgByWholPjtcoAmt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + }, // 용도별사업비 > 처리장 - { name: 'PRCPLT_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'tmplntWholAmt', + fieldName: 'tmplntWholAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['tmplntFcltAmt', 'tmplntMendngAmt', 'tmplntOperKeepAmt', 'tmplntEtcAmt'], + type: 'sum' as const, + }, + }, + { name: 'tmplntFcltAmt', fieldName: 'tmplntFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntMendngAmt', fieldName: 'tmplntMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntOperKeepAmt', fieldName: 'tmplntOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntEtcAmt', fieldName: 'tmplntEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 하수관로 - { name: 'SEWAGE_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSwagePplAmt', + fieldName: 'wholSwagePplAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'swagePplFcltAmt', + 'swagePplMendngAmt', + 'swagePplOperKeepAmt', + 'swagePplEtcAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'swagePplFcltAmt', fieldName: 'swagePplFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplMendngAmt', fieldName: 'swagePplMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplOperKeepAmt', fieldName: 'swagePplOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplEtcAmt', fieldName: 'swagePplEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 하수저류시설 - { name: 'SWGE_STRGE_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSwageImpdFcltAmt', + fieldName: 'wholSwageImpdFcltAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'swageImpdFcltAmt', + 'swageImpdMendngAmt', + 'swageImpdOperKeepAmt', + 'swageImpdFcltEtcAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'swageImpdFcltAmt', fieldName: 'swageImpdFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdMendngAmt', fieldName: 'swageImpdMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdOperKeepAmt', fieldName: 'swageImpdOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdFcltEtcAmt', fieldName: 'swageImpdFcltEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 기타 - { name: 'ETC_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ACDNT_CYFD_AMOUNT', fieldName: '사고이월액', header: { text: '사고이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'UNRCIV_AMOUNT', fieldName: '미수납액', header: { text: '미수납액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_AMOUNT', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'etcWholAmt', + fieldName: 'etcWholAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['etcAcdntCrfwAmt', 'etcNrcpAmt', 'etcAmt'], + type: 'sum' as const, + }, + }, + { name: 'etcAcdntCrfwAmt', fieldName: 'etcAcdntCrfwAmt', header: { text: '사고이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcNrcpAmt', fieldName: 'etcNrcpAmt', header: { text: '미수납액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcAmt', fieldName: 'etcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 참조 필드 - { name: 'B_LEVY_AMOUNT', fieldName: 'B_LEVY_AMOUNT', header: { text: 'B_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'C_LEVY_AMOUNT', fieldName: 'C_LEVY_AMOUNT', header: { text: 'C_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'D_LEVY_AMOUNT', fieldName: 'D_LEVY_AMOUNT', header: { text: 'D_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_finance', header: { text: '재정별 사업비(백만원)' }, items: [ - 'FNANC_WCT_SM', - 'NTNXPN', + 'fnnByWholPjtcoAmt', + 'cnterGovSprtAmt', { name: 'group_local_finance', header: { text: '지방비' }, - items: ['LCLEXPNS', 'LCLTY_CT'] + items: ['mctpvLcexpAmt', 'wareaSggLclAmt'] }, - 'SEWER_RNTFEE', - 'AMNTBRNCSR', - 'ETC_INCME', - 'CYFD_AMOUNT' + 'sewerUseAmt', + 'orgprBrdnAmt', + 'etcIncmAmt', + 'crfwAmt' ] }, - 'DEBT_AMOUNT', + 'lbltAmt', { name: 'group_purpose', header: { text: '용도별사업비(백만원)' }, items: [ - 'PRPOS_WCT_SM', + 'usgByWholPjtcoAmt', { name: 'group_treatment', header: { text: '처리장' }, items: [ - 'PRCPLT_SM', - 'PRCPLT_FCLTY', - 'PRCPLT_RPAIR', - 'PRCPLT_ORPNS', - 'PRCPLT_ETC' + 'tmplntWholAmt', + 'tmplntFcltAmt', + 'tmplntMendngAmt', + 'tmplntOperKeepAmt', + 'tmplntEtcAmt' ] }, { name: 'group_pipe', header: { text: '하수관로' }, items: [ - 'SEWAGE_SM', - 'SEWAGE_FCLTY', - 'SEWAGE_RPAIR', - 'SEWAGE_ORPNS', - 'SEWAGE_ETC' + 'wholSwagePplAmt', + 'swagePplFcltAmt', + 'swagePplMendngAmt', + 'swagePplOperKeepAmt', + 'swagePplEtcAmt' ] }, { name: 'group_storage', header: { text: '하수저류시설' }, items: [ - 'SWGE_STRGE_SM', - 'SWGE_STRGE_FCLTY', - 'SWGE_STRGE_RPAIR', - 'SWGE_STRGE_ORPNS', - 'SWGE_STRGE_ETC' + 'wholSwageImpdFcltAmt', + 'swageImpdFcltAmt', + 'swageImpdMendngAmt', + 'swageImpdOperKeepAmt', + 'swageImpdFcltEtcAmt' ] }, { name: 'group_etc_purpose', header: { text: '기타' }, items: [ - 'ETC_SM', - 'ACDNT_CYFD_AMOUNT', - 'UNRCIV_AMOUNT', - 'ETC_AMOUNT' + 'etcWholAmt', + 'etcAcdntCrfwAmt', + 'etcNrcpAmt', + 'etcAmt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewerageFinanceSettleInp', + tabKey: 'ST00090001', tabLabel: '하수도재정 결산' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts index bd4be4668..f3e03d163 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts @@ -5,67 +5,108 @@ import { ValueType } from "realgrid"; export const SewerageUsageFeeInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 총계 > 부과액 - { name: 'LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'levyAmt', + fieldName: 'levyAmt', + header: { text: '(백만원)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: [ + 'hshusLevyAmt', + 'taskFrLevyAmt', + 'fbuLevyAmt', + 'bthhsFrSpcs1LevyAmt', + 'bthhsFrSpcs2LevyAmt', + 'induLevyAmt', + 'etcLevyAmt', + ], + type: 'sum' as const, + }, + }, // 총계 > 부과량 - { name: 'LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'levyQnt', + fieldName: 'levyQnt', + header: { text: '(천톤)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: [ + 'hshusLevyQnt', + 'taskFrLevyQnt', + 'fbuLevyQnt', + 'bthhsFrSpcs1LevyQnt', + 'bthhsFrSpcs2LevyQnt', + 'induLevyQnt', + 'etcLevyQnt', + ], + type: 'sum' as const, + }, + }, // 가정용 - { name: 'HOME_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'HOME_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'HOME_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusLevyAmt', fieldName: 'hshusLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusLevyQnt', fieldName: 'hshusLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 업무용 - { name: 'JOB_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'JOB_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'JOB_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrLevyAmt', fieldName: 'taskFrLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrLevyQnt', fieldName: 'taskFrLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 영업용(일반용) - { name: 'BSN_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BSN_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BSN_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuLevyAmt', fieldName: 'fbuLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuLevyQnt', fieldName: 'fbuLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 욕탕1종(대중탕용) - { name: 'BATHS1_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BATHS1_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1LevyAmt', fieldName: 'bthhsFrSpcs1LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1LevyQnt', fieldName: 'bthhsFrSpcs1LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 욕탕2종 - { name: 'PBBATH_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PBBATH_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PBBATH_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2LevyAmt', fieldName: 'bthhsFrSpcs2LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2LevyQnt', fieldName: 'bthhsFrSpcs2LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 산업 및 공업용 - { name: 'INDUST_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INDUST_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induLevyAmt', fieldName: 'induLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induLevyQnt', fieldName: 'induLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 기타 - { name: 'ETC_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyAmt', fieldName: 'etcLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyQnt', fieldName: 'etcLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - - // 참조 필드 - { name: 'SEWER_RNTFEE', fieldName: 'SEWER_RNTFEE', header: { text: 'SEWER_RNTFEE' } }, - { name: 'B_LEVY_AMOUNT', fieldName: 'B_LEVY_AMOUNT', header: { text: 'B_LEVY_AMOUNT' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_total', @@ -74,12 +115,12 @@ export const SewerageUsageFeeInpColumn = { { name: 'total_amount', header: { text: '부과액' }, - items: ['LEVY_AMOUNT'] + items: ['levyAmt'] }, { name: 'total_qty', header: { text: '부과량' }, - items: ['LEVY_QY'] + items: ['levyQnt'] } ] }, @@ -90,17 +131,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'home_amount', header: { text: '부과액' }, - items: ['HOME_LEVY_AMOUNT'] + items: ['hshusLevyAmt'] }, { name: 'home_qty', header: { text: '부과량' }, - items: ['HOME_LEVY_QY'] + items: ['hshusLevyQnt'] }, { name: 'home_price', header: { text: '단가' }, - items: ['HOME_UNTPC'] + items: ['hshusUntprcAmt'] } ] }, @@ -111,17 +152,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'job_amount', header: { text: '부과액' }, - items: ['JOB_LEVY_AMOUNT'] + items: ['taskFrLevyAmt'] }, { name: 'job_qty', header: { text: '부과량' }, - items: ['JOB_LEVY_QY'] + items: ['taskFrLevyQnt'] }, { name: 'job_price', header: { text: '단가' }, - items: ['JOB_UNTPC'] + items: ['taskFrUntprcAmt'] } ] }, @@ -132,17 +173,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'bsn_amount', header: { text: '부과액' }, - items: ['BSN_LEVY_AMOUNT'] + items: ['fbuLevyAmt'] }, { name: 'bsn_qty', header: { text: '부과량' }, - items: ['BSN_LEVY_QY'] + items: ['fbuLevyQnt'] }, { name: 'bsn_price', header: { text: '단가' }, - items: ['BSN_UNTPC'] + items: ['fbuUntprcAmt'] } ] }, @@ -153,17 +194,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'baths1_amount', header: { text: '부과액' }, - items: ['BATHS1_LEVY_AMOUNT'] + items: ['bthhsFrSpcs1LevyAmt'] }, { name: 'baths1_qty', header: { text: '부과량' }, - items: ['BATHS1_LEVY_QY'] + items: ['bthhsFrSpcs1LevyQnt'] }, { name: 'baths1_price', header: { text: '단가' }, - items: ['BATHS1_UNTPC'] + items: ['bthhsFrSpcs1UntprcAmt'] } ] }, @@ -174,17 +215,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'pbbath_amount', header: { text: '부과액' }, - items: ['PBBATH_LEVY_AMOUNT'] + items: ['bthhsFrSpcs2LevyAmt'] }, { name: 'pbbath_qty', header: { text: '부과량' }, - items: ['PBBATH_LEVY_QY'] + items: ['bthhsFrSpcs2LevyQnt'] }, { name: 'pbbath_price', header: { text: '단가' }, - items: ['PBBATH_UNTPC'] + items: ['bthhsFrSpcs2UntprcAmt'] } ] }, @@ -195,17 +236,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'indust_amount', header: { text: '부과액' }, - items: ['INDUST_LEVY_AMOUNT'] + items: ['induLevyAmt'] }, { name: 'indust_qty', header: { text: '부과량' }, - items: ['INDUST_LEVY_QY'] + items: ['induLevyQnt'] }, { name: 'indust_price', header: { text: '단가' }, - items: ['INDUST_UNTPC'] + items: ['induUntprcAmt'] } ] }, @@ -216,25 +257,27 @@ export const SewerageUsageFeeInpColumn = { { name: 'etc_amount', header: { text: '부과액' }, - items: ['ETC_LEVY_AMOUNT'] + items: ['etcLevyAmt'] }, { name: 'etc_qty', header: { text: '부과량' }, - items: ['ETC_LEVY_QY'] + items: ['etcLevyQnt'] }, { name: 'etc_price', header: { text: '단가' }, - items: ['ETC_UNTPC'] + items: ['etcLevyUntprcAmt'] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewerageUsageFeeInp', + tabKey: 'ST00090003', tabLabel: '하수도 사용료' }; \ No newline at end of file diff --git a/apps/st/src/components/util/grid/computedFieldCallbacks.ts b/apps/st/src/components/util/grid/computedFieldCallbacks.ts index 65aa811ca..f8c59f37a 100644 --- a/apps/st/src/components/util/grid/computedFieldCallbacks.ts +++ b/apps/st/src/components/util/grid/computedFieldCallbacks.ts @@ -43,6 +43,65 @@ export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCall }; } +function sumFieldValues( + fields: string[], + fieldNamesArr: string[], + values: unknown[], +): number { + let sum = 0; + for (const f of fields) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) sum += toNum(values[idx]); + } + return sum; +} + +/** halfSumOfSums 한 건 — (sum(first) + sum(second)) / 2, 소수 내림 */ +function computeHalfSumOfSums( + firstSumFields: string[], + secondSumFields: string[], + fieldNamesArr: string[], + values: unknown[], +): number { + const firstSum = sumFieldValues(firstSumFields, fieldNamesArr, values); + const secondSum = sumFieldValues(secondSumFields, fieldNamesArr, values); + return Math.floor((firstSum + secondSum) / 2); +} + +export type HalfSumOfSumsGroup = { + firstSumFields: string[]; + secondSumFields: string[]; +}; + +/** 두 그룹 합계의 평균 — (sum(first) + sum(second)) / 2 */ +export function createHalfSumOfSumsValueCallback( + firstSumFields: string[], + secondSumFields: string[], +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => + computeHalfSumOfSums(firstSumFields, secondSumFields, fieldNamesArr, values); +} + +/** halfSumOfSums 2건의 합 — (B+C)/2 + (E+F)/2 */ +export function createSumOfTwoHalfSumOfSumsValueCallback( + firstGroup: HalfSumOfSumsGroup, + secondGroup: HalfSumOfSumsGroup, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => + computeHalfSumOfSums( + firstGroup.firstSumFields, + firstGroup.secondSumFields, + fieldNamesArr, + values, + ) + + computeHalfSumOfSums( + secondGroup.firstSumFields, + secondGroup.secondSumFields, + fieldNamesArr, + values, + ); +} + /** 차이값: fields[0] − fields[1] (예: 매설연장 − 철거연장) */ export function createDifferenceValueCallback(fields: string[]): DataFieldValueCallback { const a = fields[0] ?? ""; @@ -179,6 +238,21 @@ export function createSwrgInstlrtRateValueCallback( }; } +/** 처리효율: (유입BOD − 방류BOD) / 유입BOD */ +export function createBodPrcsRtValueCallback( + inflwField: string, + dschgField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const inflwIdx = fieldNamesArr.indexOf(inflwField); + const dschgIdx = fieldNamesArr.indexOf(dschgField); + const inflw = inflwIdx >= 0 ? toNum(values[inflwIdx]) : 0; + const dschg = dschgIdx >= 0 ? toNum(values[dschgIdx]) : 0; + if (inflw === 0) return 0; + return (inflw - dschg) / inflw; + }; +} + /** 총양수량: Σ(펌프대수 + 예비펌프대수) × 대당 양수량 */ export function createPumpGrossPpqtyValueCallback( pumpGroups: Array<{ @@ -202,6 +276,44 @@ export function createPumpGrossPpqtyValueCallback( }; } +/** (a/b) / (c/d) × 100 — 각 quotient 분모 0이면 0 */ +export function createQuotientRatioPercentValueCallback( + numeratorQuotient: [string, string], + denominatorQuotient: [string, string], +): DataFieldValueCallback { + const [numField, numDenField] = numeratorQuotient; + const [denNumField, denDenField] = denominatorQuotient; + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const getVal = (field: string) => { + const idx = fieldNamesArr.indexOf(field); + return idx >= 0 ? toNum(values[idx]) : 0; + }; + const numDen = getVal(numDenField); + const denDen = getVal(denDenField); + const denNum = getVal(denNumField); + if (numDen === 0 || denDen === 0 || denNum === 0) return 0; + const numerator = getVal(numField) / numDen; + const denominator = denNum / denDen; + if (denominator === 0) return 0; + return (numerator / denominator) * 100; + }; +} + +/** 나눗셈: fields[0] / fields[1] (분모 0이면 0) */ +export function createQuotientValueCallback(fields: string[]): DataFieldValueCallback { + const numeratorField = fields[0] ?? ""; + const denominatorField = fields[1] ?? ""; + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + if (!numeratorField || !denominatorField) return 0; + const numIdx = fieldNamesArr.indexOf(numeratorField); + const denIdx = fieldNamesArr.indexOf(denominatorField); + const numerator = numIdx >= 0 ? toNum(values[numIdx]) : 0; + const denominator = denIdx >= 0 ? toNum(values[denIdx]) : 0; + if (denominator === 0) return 0; + return numerator / denominator; + }; +} + /** 연산 타입에 맞는 valueCallback 반환 */ export function getComputedValueCallback(config: ComputedFieldConfig): DataFieldValueCallback { switch (config.type) { @@ -213,11 +325,28 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createMaxValueCallback(config.fields); case 'difference': return createDifferenceValueCallback(config.fields); + case 'quotient': + return createQuotientValueCallback(config.fields); + case 'quotientRatioPercent': + return createQuotientRatioPercentValueCallback( + config.numeratorQuotient, + config.denominatorQuotient, + ); case 'differenceOfSums': return createDifferenceOfSumsValueCallback( config.minuendFields, config.subtrahendFields, ); + case 'halfSumOfSums': + return createHalfSumOfSumsValueCallback( + config.firstSumFields, + config.secondSumFields, + ); + case 'sumOfTwoHalfSumOfSums': + return createSumOfTwoHalfSumOfSumsValueCallback( + config.firstHalfSum, + config.secondHalfSum, + ); case 'maxOfFieldAndSum': return createMaxOfFieldAndSumValueCallback( config.baseField, @@ -234,5 +363,7 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createSwrgInstlrtRateValueCallback(config.numeratorFields, config.denominatorFields, config.thirdFields); case 'pumpGrossPpqty': return createPumpGrossPpqtyValueCallback(config.pumpGroups); + case 'bodPrcsRt': + return createBodPrcsRtValueCallback(config.inflwField, config.dschgField); } } diff --git a/apps/st/src/components/util/gridExcelExport.ts b/apps/st/src/components/util/gridExcelExport.ts index ce86d6492..983d4ba5a 100644 --- a/apps/st/src/components/util/gridExcelExport.ts +++ b/apps/st/src/components/util/gridExcelExport.ts @@ -1,6 +1,64 @@ -import type { ColumnConfig } from "@/components/type/DataGrid.type"; +import type { + ColumnConfig, + ColumnLayoutItem, +} from "@/components/type/DataGrid.type"; import type { GridView } from "realgrid"; +function flattenColumnLayoutNames(layout: ColumnLayoutItem[]): string[] { + const result: string[] = []; + for (const item of layout) { + if (typeof item === "string") { + result.push(item); + continue; + } + if (item && typeof item === "object") { + if ("column" in item && typeof item.column === "string") { + result.push(item.column); + continue; + } + if ("items" in item && Array.isArray(item.items)) { + result.push( + ...flattenColumnLayoutNames( + item.items as ColumnLayoutItem[], + ), + ); + } + } + } + return result; +} + +/** 엑셀 가져오기 — N번째 열을 그리드 N번째 fieldName에 매핑 (보내기 컬럼 순서와 동일) */ +export function getExcelImportColumnFieldNames( + columns: ColumnConfig[], + columnLayout?: ColumnLayoutItem[], +): string[] { + const excludedNames = new Set( + columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), + ); + const fieldByName = new Map( + columns + .filter((c) => c.fieldName) + .map((c) => [c.name, c.fieldName as string]), + ); + + const layoutNames = + columnLayout && columnLayout.length > 0 + ? flattenColumnLayoutNames(columnLayout) + : columns.map((c) => c.name); + + const seen = new Set(); + const fieldNames: string[] = []; + for (const name of layoutNames) { + if (excludedNames.has(name) || seen.has(name)) continue; + const fieldName = fieldByName.get(name); + if (!fieldName) continue; + seen.add(name); + fieldNames.push(fieldName); + } + return fieldNames; +} + export type ExcelExportVisibilitySnapshot = Map; /** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(visible:false) 컬럼은 export에만 표시 */ diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts new file mode 100644 index 000000000..e4489bc1a --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts @@ -0,0 +1,53 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type ExcretaTreatmentFacilityApiResponse, + type ExcretaTreatmentFacilityRequestParams, + type ExcretaTreatmentFacilitySaveRequest, + type ExcretaTreatmentFacilitySaveResponse, + type ExcretaTreatmentFacilitySubmitRequest, + type ExcretaTreatmentFacilitySubmitResponse, + type StatisticsInputExcretaTreatmentFacilityService, +} from "../../model/input/statistics-input-excreta-treatment-facility-interface"; + +export class StatisticsInputExcretaTreatmentFacilityApiService + implements StatisticsInputExcretaTreatmentFacilityService +{ + /** 분뇨처리시설 조회 — ST00070001 */ + public async getExcretaTreatmentFacilityList( + params: ExcretaTreatmentFacilityRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨처리시설 저장 — ST00070001 */ + public async saveExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility/save`, + request, + ); + return response.data; + } + + /** 분뇨처리시설 제출 — ST00070001 */ + public async submitExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputExcretaTreatmentFacilityApi = + new StatisticsInputExcretaTreatmentFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts new file mode 100644 index 000000000..bcb23018c --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts @@ -0,0 +1,133 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PrivateSewageDesigStatusApiResponse, + type PrivateSewageDesigStatusRequestParams, + type PrivateSewageDesigStatusSaveRequest, + type PrivateSewageDesigStatusSaveResponse, + type PrivateSewageDesigStatusSubmitRequest, + type PrivateSewageDesigStatusSubmitResponse, + type SepticTankStatusApiResponse, + type SepticTankStatusRequestParams, + type SepticTankStatusSaveRequest, + type SepticTankStatusSaveResponse, + type SepticTankStatusSubmitRequest, + type SepticTankStatusSubmitResponse, + type SewageTreatFactStatusApiResponse, + type SewageTreatFactStatusRequestParams, + type SewageTreatFactStatusSaveRequest, + type SewageTreatFactStatusSaveResponse, + type SewageTreatFactStatusSubmitRequest, + type SewageTreatFactStatusSubmitResponse, + type StatisticsInputPrivateSewageFacilityService, +} from "../../model/input/statistics-input-private-sewage-facility-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/private-sewage-facility`; + +export class StatisticsInputPrivateSewageFacilityApiService + implements StatisticsInputPrivateSewageFacilityService +{ + /** 처리용량별 오수처리시설 현황 조회 — ST00080001 */ + public async getSewageTreatFactStatusList( + params: SewageTreatFactStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/sewage-treat-fact-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 처리용량별 오수처리시설 현황 저장 — ST00080001 */ + public async saveSewageTreatFactStatus( + request: SewageTreatFactStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-treat-fact-status/save`, + request, + ); + return response.data; + } + + /** 처리용량별 오수처리시설 현황 제출 — ST00080001 */ + public async submitSewageTreatFactStatus( + request: SewageTreatFactStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-treat-fact-status/submit`, + request, + ); + return response.data; + } + + /** 정화조 현황 조회 — ST00080002 */ + public async getSepticTankStatusList( + params: SepticTankStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/septic-tank-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 정화조 현황 저장 — ST00080002 */ + public async saveSepticTankStatus( + request: SepticTankStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/septic-tank-status/save`, + request, + ); + return response.data; + } + + /** 정화조 현황 제출 — ST00080002 */ + public async submitSepticTankStatus( + request: SepticTankStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/septic-tank-status/submit`, + request, + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 조회 — ST00080003 */ + public async getPrivateSewageDesigStatusList( + params: PrivateSewageDesigStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/desig-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 저장 — ST00080003 */ + public async savePrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/desig-status/save`, + request, + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 제출 — ST00080003 */ + public async submitPrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/desig-status/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputPrivateSewageFacilityApi = + new StatisticsInputPrivateSewageFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts new file mode 100644 index 000000000..7538acb7d --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts @@ -0,0 +1,50 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PumpingStationApiResponse, + type PumpingStationRequestParams, + type PumpingStationSaveRequest, + type PumpingStationSaveResponse, + type PumpingStationSubmitRequest, + type PumpingStationSubmitResponse, + type StatisticsInputPumpingStationService, +} from "../../model/input/statistics-input-pumping-station-interface"; + +export class StatisticsInputPumpingStationApiService implements StatisticsInputPumpingStationService { + /** 펌프장 조회 — ST00050001 */ + public async getPumpingStationList( + params: PumpingStationRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 펌프장 저장 — ST00050001 */ + public async savePumpingStationList( + request: PumpingStationSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station/save`, + request, + ); + return response.data; + } + + /** 펌프장 제출 — ST00050001 */ + public async submitPumpingStationList( + request: PumpingStationSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputPumpingStationApi = new StatisticsInputPumpingStationApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts index 78dd39e9c..fce1895d4 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts @@ -1,5 +1,6 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; import { type StatisticsInputSewSupplyRateService, @@ -15,7 +16,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe public async getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, - { params } + withAuthHeader({ params }), ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts index 49229becb..cc366f08d 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts @@ -1,4 +1,5 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import type { @@ -20,7 +21,7 @@ export class SewagePipeMaintainApiService implements SewagePipeMaintainService { public async getSewagePipeMaintainRepair(param: SewagePipeMaintainRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/repair`, - { params: param } + withAuthHeader({ params: param }) ); return response.data; }; @@ -28,7 +29,7 @@ export class SewagePipeMaintainApiService implements SewagePipeMaintainService { public async getSewagePipeMaintainDredging(param: SewagePipeMaintainRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/dredging`, - { params: param } + withAuthHeader({ params: param }) ); return response.data; }; diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts index 764a27cde..9e187ce30 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts @@ -1,4 +1,5 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import { @@ -7,7 +8,13 @@ import { type SewageReservoirRequestParams, type SewageReservoirSaveRequest, type SewageReservoirSaveResponse, + type SewageReservoirSubmitRequest, + type SewageReservoirSubmitResponse, type RainfallPumpStationApiResponse, + type RainfallPumpStationSaveRequest, + type RainfallPumpStationSaveResponse, + type RainfallPumpStationSubmitRequest, + type RainfallPumpStationSubmitResponse, } from "../../model/input/statistics-input-sewage-reservoir-pump-interface"; export class StatisticsInputSewageReservoirPumpApiService implements StatisticsInputSewageReservoirPumpService { @@ -15,7 +22,7 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI public async getSewageReservoirList(params: SewageReservoirRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir`, - { params }, + withAuthHeader({ params }), ); return response.data; } @@ -26,7 +33,7 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump`, - { params }, + withAuthHeader({ params }), ); return response.data; } @@ -41,6 +48,39 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI ); return response.data; } + + /** 하수저류시설 제출 — ST00040001 */ + public async submitSewageReservoir( + request: SewageReservoirSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir/submit`, + request, + ); + return response.data; + } + + /** 빗물펌프장 저장 — ST00040002 */ + public async saveRainfallPumpStation( + request: RainfallPumpStationSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump/save`, + request, + ); + return response.data; + } + + /** 빗물펌프장 제출 — ST00040002 */ + public async submitRainfallPumpStation( + request: RainfallPumpStationSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump/submit`, + request, + ); + return response.data; + } } export const statisticsInputSewageReservoirPumpApi = new StatisticsInputSewageReservoirPumpApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts new file mode 100644 index 000000000..0b6a919f0 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts @@ -0,0 +1,68 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type CauserBurdenApiResponse, + type CauserBurdenRequestParams, + type SewerageChargeApiResponse, + type SewerageChargeRequestParams, + type SewerageFinanceSettleApiResponse, + type SewerageFinanceSettleRequestParams, + type SewerageUsageFeeApiResponse, + type SewerageUsageFeeRequestParams, + type StatisticsInputSewerageFinanceService, +} from "../../model/input/statistics-input-sewerage-finance-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-finance`; + +export class StatisticsInputSewerageFinanceApiService + implements StatisticsInputSewerageFinanceService +{ + /** 하수도재정 결산 조회 — ST00090001 */ + public async getSewerageFinanceSettleList( + params: SewerageFinanceSettleRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/settle`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수도 요금 조회 — ST00090002 */ + public async getSewerageChargeList( + params: SewerageChargeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/charge`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수도 사용료 조회 — ST00090003 */ + public async getSewerageUsageFeeList( + params: SewerageUsageFeeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/usage-fee`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 원인자 부담금 조회 — ST00090004 */ + public async getCauserBurdenList( + params: CauserBurdenRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/causer-burden`, + withAuthHeader({ params }), + ); + return response.data; + } +} + +export const statisticsInputSewerageFinanceApi = + new StatisticsInputSewerageFinanceApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts index 29b766b06..b0cd77d3a 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts @@ -1,4 +1,5 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import type { @@ -20,7 +21,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics public async getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -28,7 +29,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics public async getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -38,7 +39,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/mapping-targets`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } 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 24b271824..c506a5085 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 @@ -1,5 +1,6 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; import type { SeweragePipeFacilityAdminAreaListRequest, SeweragePipeFacilityAdminAreaListResponse, @@ -39,7 +40,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getRegionalFactStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -47,7 +48,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFactInstallStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -55,7 +56,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getPipeTypeStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -63,7 +64,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getPipeDiameterStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -71,7 +72,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getManholeStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -79,7 +80,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getConnectionStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -87,7 +88,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFacilitiesList(param: SeweragePipeFacilitiesListRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/facilities`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -95,7 +96,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/areas`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts new file mode 100644 index 000000000..662c2d30a --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts @@ -0,0 +1,166 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 분뇨처리시설(ST00070001) — `ExcretaTreatFactInpColumn` */ +export const EXCRETA_TREATMENT_FACILITY_STATS_CD = "ST00070001" as const; + +/** 검증 결과 한 건 */ +export interface ExcretaTreatmentFacilityVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type ExcretaTreatmentFacilityVerificationResult = Record< + string, + ExcretaTreatmentFacilityVerificationResultItem[] +>; + +/** 분뇨처리시설 조회 `data.commonInfo` */ +export interface ExcretaTreatmentFacilityCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +} + +/** + * 분뇨처리시설 조회 `data.excretaTreatmentFacilityList[]` 한 행. + * `ExcretaTreatFactInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaTreatmentFacilityItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + fcltPstnCn: string; + telNo: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string; + linkTmplntCn: string; + linkPrcsQnt: number; + inflwBodWqVl: number; + inflwTocWqVl: number; + inflwSsWqVl: number; + inflwTnWqVl: number; + inflwTpWqVl: number; + inflwClfgCnt: number; + dschgBodWqVl: number; + dschgTocWqVl: number; + dschgSsWqVl: number; + dschgTnWqVl: number; + dschgTpWqVl: number; + dschgClfgCnt: number; + wtsysNm: string; + trbtrNm: string; + msirbCn: string; + ssirbCn: string; + operMthdCdCn: string; + wholNope: number; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +} + +export interface ExcretaTreatmentFacilityResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: ExcretaTreatmentFacilityCommonInfo; + excretaTreatmentFacilityList: ExcretaTreatmentFacilityItem[]; +} + +export type ExcretaTreatmentFacilityApiResponse = + ApiResponse & { + message: string; + }; + +/** 분뇨처리시설(ST00070001) 조회 요청 파라미터 */ +export interface ExcretaTreatmentFacilityRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; + fcltNm?: string; +} + +/** + * 분뇨처리시설(ST00070001) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·검증결과는 저장 요청에서 제외. + */ +export type ExcretaTreatmentFacilitySaveItem = Omit< + ExcretaTreatmentFacilityItem, + "sggNm" | "verificationResult" +>; + +/** 분뇨처리시설(ST00070001) 저장 API 요청 */ +export interface ExcretaTreatmentFacilitySaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: ExcretaTreatmentFacilitySaveItem[]; + deleteList: ExcretaTreatmentFacilitySaveItem[]; +} + +export interface ExcretaTreatmentFacilitySaveResponse { + status: string; + message?: string; + data?: string; +} + +export type ExcretaTreatmentFacilitySubmitItem = ExcretaTreatmentFacilitySaveItem; + +/** 분뇨처리시설(ST00070001) 제출 API 요청 */ +export interface ExcretaTreatmentFacilitySubmitRequest { + 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; + + superAdmin?: string; + noApplicable?: boolean; + submitList: ExcretaTreatmentFacilitySubmitItem[]; + deleteList: ExcretaTreatmentFacilitySubmitItem[]; +} + +export interface ExcretaTreatmentFacilitySubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputExcretaTreatmentFacilityService { + getExcretaTreatmentFacilityList( + params: ExcretaTreatmentFacilityRequestParams, + ): Promise; + saveExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySaveRequest, + ): Promise; + submitExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts new file mode 100644 index 000000000..89179711b --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts @@ -0,0 +1,453 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 처리용량별 오수처리시설 현황(ST00080001) — `SewageTreatFactStatusInpColumn.tabKey` */ +export const SEWAGE_TREAT_FACT_STATUS_STATS_CD = "ST00080001" as const; + +/** 검증 결과 한 건 */ +export interface PrivateSewageFacilityVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type PrivateSewageFacilityVerificationResult = Record< + string, + PrivateSewageFacilityVerificationResultItem[] +>; + +/** ST00080001 조회 `data.commonInfo` */ +export interface SewageTreatFactStatusCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080001 조회 `data.sewageTreatFactStatusList[]` 한 행. + * `SewageTreatFactStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewageTreatFactStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + wholDrtwPrcsFcltCnt: number; + wholStaaCnt: number; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + staaExcpWhol: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface SewageTreatFactStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageTreatFactStatusCommonInfo; + sewageTreatFactStatusList: SewageTreatFactStatusItem[]; +} + +export type SewageTreatFactStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00080001 조회 요청 파라미터 */ +export interface SewageTreatFactStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 정화조 현황(ST00080002) — `SepticTankStatusInpColumn.tabKey` */ +export const SEPTIC_TANK_STATUS_STATS_CD = "ST00080002" as const; + +/** ST00080002 조회 `data.commonInfo` */ +export interface SepticTankStatusCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080002 조회 `data.septicTankStatusList[]` 한 행. + * `SepticTankStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SepticTankStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + grssFcltCnt: number; + staaInsdWhol: number; + staaInsdPrcsMthByWhol: number; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + staaInsdPrcsCpctByWhol: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + staaExcpWhol: number; + staaExcpPrcsMthByWhol: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + staaExcpPrcsCpctByWhol: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface SepticTankStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SepticTankStatusCommonInfo; + septicTankStatusList: SepticTankStatusItem[]; +} + +export type SepticTankStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00080002 조회 요청 파라미터 */ +export interface SepticTankStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 개인하수도 관리지역 지정공고 현황(ST00080003) — `PrivateSewageDesigStatusInpColumn` */ +export const PRIVATE_SEWAGE_DESIG_STATUS_STATS_CD = "ST00080003" as const; + +/** ST00080003 조회 `data.commonInfo` */ +export interface PrivateSewageDesigStatusCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080003 조회 `data.privateSewageDesigStatusList[]` 한 행. + * `PrivateSewageDesigStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PrivateSewageDesigStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface PrivateSewageDesigStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PrivateSewageDesigStatusCommonInfo; + privateSewageDesigStatusList: PrivateSewageDesigStatusItem[]; +} + +export type PrivateSewageDesigStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00080003 조회 요청 파라미터 */ +export interface PrivateSewageDesigStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00080001 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·계산필드(`wholDrtwPrcsFcltCnt`, `wholStaaCnt`, `staaExcpWhol`)·검증결과는 제외. + */ +export interface SewageTreatFactStatusSaveItem { + admdstCd: string; + admdstNm: string; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; + rmrk: string | null; +} + +/** ST00080001 저장 API 요청 */ +export interface SewageTreatFactStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SewageTreatFactStatusSaveItem[]; +} + +export interface SewageTreatFactStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080001 제출 API 요청 */ +export interface SewageTreatFactStatusSubmitRequest { + 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; + superAdmin?: string; + noApplicable?: boolean; + submitList: SewageTreatFactStatusSaveItem[]; +} + +export interface SewageTreatFactStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00080002 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·계산필드(`grssFcltCnt`, `staaInsdWhol`, `staaInsdPrcsMthByWhol`, `staaInsdPrcsCpctByWhol`, + * `staaExcpWhol`, `staaExcpPrcsMthByWhol`, `staaExcpPrcsCpctByWhol`)·검증결과는 제외. + */ +export interface SepticTankStatusSaveItem { + admdstCd: string; + admdstNm: string; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; + rmrk: string | null; +} + +/** ST00080002 저장 API 요청 */ +export interface SepticTankStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SepticTankStatusSaveItem[]; +} + +export interface SepticTankStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080002 제출 API 요청 */ +export interface SepticTankStatusSubmitRequest { + 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; + superAdmin?: string; + noApplicable?: boolean; + submitList: SepticTankStatusSaveItem[]; +} + +export interface SepticTankStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00080003 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·검증결과는 제외. + */ +export interface PrivateSewageDesigStatusSaveItem { + admdstCd: string; + admdstNm: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; +} + +/** ST00080003 저장 API 요청 */ +export interface PrivateSewageDesigStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PrivateSewageDesigStatusSaveItem[]; +} + +export interface PrivateSewageDesigStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080003 제출 API 요청 */ +export interface PrivateSewageDesigStatusSubmitRequest { + 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; + superAdmin?: string; + noApplicable?: boolean; + submitList: PrivateSewageDesigStatusSaveItem[]; +} + +export interface PrivateSewageDesigStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputPrivateSewageFacilityService { + getSewageTreatFactStatusList( + params: SewageTreatFactStatusRequestParams, + ): Promise; + saveSewageTreatFactStatus( + request: SewageTreatFactStatusSaveRequest, + ): Promise; + submitSewageTreatFactStatus( + request: SewageTreatFactStatusSubmitRequest, + ): Promise; + getSepticTankStatusList( + params: SepticTankStatusRequestParams, + ): Promise; + saveSepticTankStatus( + request: SepticTankStatusSaveRequest, + ): Promise; + submitSepticTankStatus( + request: SepticTankStatusSubmitRequest, + ): Promise; + getPrivateSewageDesigStatusList( + params: PrivateSewageDesigStatusRequestParams, + ): Promise; + savePrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSaveRequest, + ): Promise; + submitPrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts new file mode 100644 index 000000000..4114fc728 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts @@ -0,0 +1,174 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 펌프장(ST00050001) — `PumpingStationInpColumn` tabKey */ +export const PUMPING_STATION_STATS_CD = "ST00050001" as const; + +/** 검증 결과 한 건 */ +export interface PumpingStationVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type PumpingStationVerificationResult = Record< + string, + PumpingStationVerificationResultItem[] +>; + +/** 펌프장 조회 `data.commonInfo` */ +export interface PumpingStationCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PumpingStationVerificationResult; +} + +/** + * 펌프장 조회 `data.pumpingStationList[]` 한 행. + * `PumpingStationInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PumpingStationItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltNm: string; + fcltNm: string; + pumpSeNm: string; + fcltPstnCn: string; + oprtngStrtYmd: string; + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; + drtwPumpN2DmtrArea: number; + drtwPumpN2Ppqty: number; + drtwPumpN2Cnt: number; + drtwPumpN2RsrvCnt: number; + drtwPumpN3DmtrArea: number; + drtwPumpN3Ppqty: number; + drtwPumpN3Cnt: number; + drtwPumpN3RsrvCnt: number; + exlnPumpGrssPpqty: number; + exlnPumpN1DmtrArea: number; + exlnPumpN1Ppqty: number; + exlnPumpN1Cnt: number; + exlnPumpN1RsrvCnt: number; + exlnPumpN2DmtrArea: number; + exlnPumpN2Ppqty: number; + exlnPumpN2Cnt: number; + exlnPumpN2RsrvCnt: number; + exlnPumpN3DmtrArea: number; + exlnPumpN3Ppqty: number; + exlnPumpN3Cnt: number; + exlnPumpN3RsrvCnt: number; + anlElpwrUseQnt: number; + rmrk: string | null; + verificationResult?: PumpingStationVerificationResult; +} + +export interface PumpingStationResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PumpingStationCommonInfo; + pumpingStationList: PumpingStationItem[]; +} + +export type PumpingStationApiResponse = ApiResponse & { + message: string; +}; + +/** 펌프장(ST00050001) 조회 요청 파라미터 */ +export interface PumpingStationRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; + fcltNm?: string; +} + +/** + * 펌프장(ST00050001) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type PumpingStationSaveItem = Omit< + PumpingStationItem, + | "sggNm" + | "verificationResult" + | "exlnQnt" + | "drtwPumpGrssPpqty" + | "exlnPumpGrssPpqty" +>; + +/** 펌프장(ST00050001) 저장 API 요청 */ +export interface PumpingStationSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PumpingStationSaveItem[]; + deleteList: PumpingStationSaveItem[]; +} + +export interface PumpingStationSaveResponse { + status: string; + message?: string; + data?: string; +} + +export type PumpingStationSubmitItem = PumpingStationSaveItem; + +/** 펌프장(ST00050001) 제출 API 요청 */ +export interface PumpingStationSubmitRequest { + 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; + + superAdmin?: string; + noApplicable?: boolean; + submitList: PumpingStationSubmitItem[]; + deleteList: PumpingStationSubmitItem[]; +} + +export interface PumpingStationSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputPumpingStationService { + getPumpingStationList( + params: PumpingStationRequestParams, + ): Promise; + savePumpingStationList( + request: PumpingStationSaveRequest, + ): Promise; + submitPumpingStationList( + request: PumpingStationSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts index 15885626c..9109f76e2 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts @@ -194,10 +194,101 @@ export interface SewageReservoirSaveResponse { data?: string; } +/** 하수저류시설(ST00040001) 제출 API 요청 */ +export interface SewageReservoirSubmitRequest { + 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; + noApplicable?: boolean; + submitList: SewageReservoirSaveItem[]; + deleteList: SewageReservoirSaveItem[]; +} + +export interface SewageReservoirSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * 빗물펌프장(ST00040002) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·검증결과는 저장 요청에서 제외. + */ +export type RainfallPumpStationSaveItem = Omit< + RainfallPumpStationItem, + "sggNm" | "verificationResult" +>; + +/** + * 빗물펌프장(ST00040002) 저장 API 요청 + * (요청 shape은 예시 JSON과 동일) + */ +export interface RainfallPumpStationSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RainfallPumpStationSaveItem[]; + deleteList: RainfallPumpStationSaveItem[]; +} + +/** 빗물펌프장(ST00040002) 저장 응답 */ +export type RainfallPumpStationSaveResponse = SewageReservoirSaveResponse; + +/** 빗물펌프장(ST00040002) 제출 API 요청 */ +export interface RainfallPumpStationSubmitRequest { + 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; + noApplicable?: boolean; + submitList: RainfallPumpStationSaveItem[]; + deleteList: RainfallPumpStationSaveItem[]; +} + +/** 빗물펌프장(ST00040002) 제출 응답 */ +export type RainfallPumpStationSubmitResponse = SewageReservoirSubmitResponse; + export interface StatisticsInputSewageReservoirPumpService { getSewageReservoirList(params: SewageReservoirRequestParams): Promise; getRainfallPumpStationList( params: SewageReservoirRequestParams, ): Promise; saveSewageReservoir(request: SewageReservoirSaveRequest): Promise; + submitSewageReservoir( + request: SewageReservoirSubmitRequest, + ): Promise; + /** 빗물펌프장 저장 — ST00040002 */ + saveRainfallPumpStation( + request: RainfallPumpStationSaveRequest, + ): Promise; + /** 빗물펌프장 제출 — ST00040002 */ + submitRainfallPumpStation( + request: RainfallPumpStationSubmitRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts new file mode 100644 index 000000000..d2928353f --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -0,0 +1,277 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 하수도재정 결산(ST00090001) — `SewerageFinanceSettleInpColumn.tabKey` */ +export const SEWERAGE_FINANCE_SETTLE_STATS_CD = "ST00090001" as const; + +/** 검증 결과 한 건 */ +export interface SewerageFinanceVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewerageFinanceVerificationResult = Record< + string, + SewerageFinanceVerificationResultItem[] +>; + +/** ST00090001 조회 `data.commonInfo` */ +export interface SewerageFinanceSettleCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090001 조회 `data.sewerageFinanceSettleList[]` 한 행. + * `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageFinanceSettleItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fnnByWholPjtcoAmt: number; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + wholSwagePplAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + wholSwageImpdFcltAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageFinanceSettleResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageFinanceSettleCommonInfo; + sewerageFinanceSettleList: SewerageFinanceSettleItem[]; +} + +export type SewerageFinanceSettleApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090001 조회 요청 파라미터 */ +export interface SewerageFinanceSettleRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */ +export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const; + +/** ST00090002 조회 `data.commonInfo` */ +export interface SewerageChargeCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090002 조회 `data.sewerageChargeList[]` 한 행. + * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageChargeItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageChargeResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageChargeCommonInfo; + sewerageChargeList: SewerageChargeItem[]; +} + +export type SewerageChargeApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090002 조회 요청 파라미터 */ +export interface SewerageChargeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */ +export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const; + +/** ST00090003 조회 `data.commonInfo` */ +export interface SewerageUsageFeeCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090003 조회 `data.sewerageUsageFeeList[]` 한 행. + * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageUsageFeeItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyAmt: number; + levyQnt: number; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + fbuLevyAmt: number; + fbuLevyQnt: number; + fbuUntprcAmt: number; + bthhsFrSpcs1LevyAmt: number; + bthhsFrSpcs1LevyQnt: number; + bthhsFrSpcs1UntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induLevyAmt: number; + induLevyQnt: number; + induUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageUsageFeeResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageUsageFeeCommonInfo; + sewerageUsageFeeList: SewerageUsageFeeItem[]; +} + +export type SewerageUsageFeeApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090003 조회 요청 파라미터 */ +export interface SewerageUsageFeeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */ +export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const; + +/** ST00090004 조회 `data.commonInfo` */ +export interface CauserBurdenCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090004 조회 `data.causerBurdenList[]` 한 행. + * `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface CauserBurdenItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface CauserBurdenResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: CauserBurdenCommonInfo; + causerBurdenList: CauserBurdenItem[]; +} + +export type CauserBurdenApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090004 조회 요청 파라미터 */ +export interface CauserBurdenRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 하수도재정 조회 공통 요청 파라미터 */ +export interface SewerageFinanceRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +export interface StatisticsInputSewerageFinanceService { + getSewerageFinanceSettleList( + params: SewerageFinanceSettleRequestParams, + ): Promise; + getSewerageChargeList( + params: SewerageChargeRequestParams, + ): Promise; + getSewerageUsageFeeList( + params: SewerageUsageFeeRequestParams, + ): Promise; + getCauserBurdenList( + params: CauserBurdenRequestParams, + ): Promise; +} diff --git a/apps/st/src/shared/api/index.ts b/apps/st/src/shared/api/index.ts index b7b9d6121..4ccba4f0b 100644 --- a/apps/st/src/shared/api/index.ts +++ b/apps/st/src/shared/api/index.ts @@ -1 +1,2 @@ export { client } from "./client"; +export { withAuthHeader } from "./with-auth-header"; diff --git a/apps/st/src/shared/api/with-auth-header.ts b/apps/st/src/shared/api/with-auth-header.ts new file mode 100644 index 000000000..9f93d8407 --- /dev/null +++ b/apps/st/src/shared/api/with-auth-header.ts @@ -0,0 +1,16 @@ +import type { AxiosRequestConfig } from "axios"; + +import { getToken } from "@/entities/auth/lib/token-manager"; +import { CLAIM_NAME } from "@/shared/config"; + +export function withAuthHeader(config: T = {} as T): T { + const authorization = getToken(CLAIM_NAME); + if (!authorization) return config; + return { + ...config, + headers: { + ...config.headers, + [CLAIM_NAME]: authorization, + }, + }; +} From f21dde8f7d38bbf3f762fc560f13e5e11f1dbb16 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 15 Jun 2026 16:34:02 +0900 Subject: [PATCH 14/15] =?UTF-8?q?[st]=20-=20tab=20&=20menu=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/(with-base-layout)/layout.tsx | 23 +- .../statistics/input/SewerageFinanceInp.tsx | 304 +++++++++++++++++- .../SewerageChargeInpColumn.ts | 2 +- .../SewerageUsageFeeInpColumn.ts | 14 +- ...atistics-input-sewerage-finance-service.ts | 52 +++ ...istics-input-sewerage-finance-interface.ts | 189 +++++++++++ .../pages/statistics/ui/statistics-page.tsx | 19 +- apps/st/src/shared/lib/bbs-notice-menu.ts | 12 + apps/st/src/shared/lib/statistics-menu-url.ts | 73 +++++ .../shared/lib/stats-cd-input-navigation.ts | 3 +- .../shared/lib/use-statistics-tab-url-sync.ts | 3 +- .../src/shared/ui/statistics-page-layout.tsx | 2 +- apps/st/src/store/tab-store.ts | 17 +- 13 files changed, 690 insertions(+), 23 deletions(-) create mode 100644 apps/st/src/shared/lib/bbs-notice-menu.ts create mode 100644 apps/st/src/shared/lib/statistics-menu-url.ts diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index e849c3a1d..b3ed5c757 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -4,7 +4,7 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; @@ -25,7 +25,12 @@ import { ENDPOINT } from "@/shared/config"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync"; +import { + menuMatchesStatisticsTabId, + resolveStatisticsTabId, +} from "@/shared/lib/statistics-menu-url"; +import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu"; import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; @@ -71,6 +76,7 @@ function WithBaseLayoutInner({ }) { const pathname = usePathname(); + const router = useRouter(); const queryClient = useQueryClient(); @@ -220,7 +226,7 @@ function WithBaseLayoutInner({ for (const item of items) { - if (item.urlPath === activeTabId) { + if (menuMatchesStatisticsTabId(item, activeTabId)) { return item; @@ -308,6 +314,13 @@ function WithBaseLayoutInner({ } + if (isBbsNoticeMenu(item)) { + if (item.urlPath) { + router.push(item.urlPath); + } + return; + } + const addTabResult: boolean = addTab(item); @@ -321,9 +334,7 @@ function WithBaseLayoutInner({ addTabForGrid(item); if (item.urlPath) { - - syncTabToUrl(item.urlPath); - + syncTabToUrl(resolveStatisticsTabId(item.urlPath)); } } @@ -354,6 +365,8 @@ function WithBaseLayoutInner({ syncTabToUrl, + router, + ], ); diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index 55f1a9998..fb2ebf09b 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -8,11 +8,19 @@ import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/inp import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { CauserBurdenItem, + CauserBurdenSaveItem, + CauserBurdenSaveRequest, SewerageChargeItem, + SewerageChargeSaveItem, + SewerageChargeSaveRequest, SewerageFinanceRequestParams, SewerageFinanceSettleItem, + SewerageFinanceSettleSaveItem, + SewerageFinanceSettleSaveRequest, SewerageFinanceVerificationResult, SewerageUsageFeeItem, + SewerageUsageFeeSaveItem, + SewerageUsageFeeSaveRequest, } from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface"; import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface"; @@ -141,6 +149,163 @@ function mapRegionalListToGridRows< }); } +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (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); + }; +} + +function resolveSewerageFinanceTab(tabKey: string) { + return { + isSettleTab: tabKey === SewerageFinanceSettleInpColumn.tabKey, + isChargeTab: tabKey === SewerageChargeInpColumn.tabKey, + isUsageFeeTab: tabKey === SewerageUsageFeeInpColumn.tabKey, + isCauserBurdenTab: tabKey === CauserBurdenInpColumn.tabKey, + }; +} + +function buildSewerageFinanceSettleSaveList( + currentData: Record[], +): SewerageFinanceSettleSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"), + mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"), + wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"), + sewerUseAmt: formatNumber(d.sewerUseAmt, "sewerUseAmt"), + orgprBrdnAmt: formatNumber(d.orgprBrdnAmt, "orgprBrdnAmt"), + etcIncmAmt: formatNumber(d.etcIncmAmt, "etcIncmAmt"), + crfwAmt: formatNumber(d.crfwAmt, "crfwAmt"), + lbltAmt: formatNumber(d.lbltAmt, "lbltAmt"), + tmplntFcltAmt: formatNumber(d.tmplntFcltAmt, "tmplntFcltAmt"), + tmplntMendngAmt: formatNumber(d.tmplntMendngAmt, "tmplntMendngAmt"), + tmplntOperKeepAmt: formatNumber(d.tmplntOperKeepAmt, "tmplntOperKeepAmt"), + tmplntEtcAmt: formatNumber(d.tmplntEtcAmt, "tmplntEtcAmt"), + swagePplFcltAmt: formatNumber(d.swagePplFcltAmt, "swagePplFcltAmt"), + swagePplMendngAmt: formatNumber(d.swagePplMendngAmt, "swagePplMendngAmt"), + swagePplOperKeepAmt: formatNumber(d.swagePplOperKeepAmt, "swagePplOperKeepAmt"), + swagePplEtcAmt: formatNumber(d.swagePplEtcAmt, "swagePplEtcAmt"), + swageImpdFcltAmt: formatNumber(d.swageImpdFcltAmt, "swageImpdFcltAmt"), + swageImpdMendngAmt: formatNumber(d.swageImpdMendngAmt, "swageImpdMendngAmt"), + swageImpdOperKeepAmt: formatNumber(d.swageImpdOperKeepAmt, "swageImpdOperKeepAmt"), + swageImpdFcltEtcAmt: formatNumber(d.swageImpdFcltEtcAmt, "swageImpdFcltEtcAmt"), + etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"), + etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"), + etcAmt: formatNumber(d.etcAmt, "etcAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"), + usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"), + tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"), + wholSwagePplAmt: formatNumber(d.wholSwagePplAmt, "wholSwagePplAmt"), + wholSwageImpdFcltAmt: formatNumber(d.wholSwageImpdFcltAmt, "wholSwageImpdFcltAmt"), + etcWholAmt: formatNumber(d.etcWholAmt, "etcWholAmt"), + })); +} + +function buildSewerageChargeSaveList( + currentData: Record[], +): SewerageChargeSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"), + grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"), + rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"), + })); +} + +function buildSewerageUsageFeeSaveList( + currentData: Record[], +): SewerageUsageFeeSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"), + hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"), + hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"), + taskFrLevyAmt: formatNumber(d.taskFrLevyAmt, "taskFrLevyAmt"), + taskFrLevyQnt: formatNumber(d.taskFrLevyQnt, "taskFrLevyQnt"), + taskFrUntprcAmt: formatNumber(d.taskFrUntprcAmt, "taskFrUntprcAmt"), + fbuLevyAmt: formatNumber(d.fbuLevyAmt, "fbuLevyAmt"), + fbuLevyQnt: formatNumber(d.fbuLevyQnt, "fbuLevyQnt"), + fbuUntprcAmt: formatNumber(d.fbuUntprcAmt, "fbuUntprcAmt"), + bthhsFrSpcs1LevyAmt: formatNumber(d.bthhsFrSpcs1LevyAmt, "bthhsFrSpcs1LevyAmt"), + bthhsFrSpcs1LevyQnt: formatNumber(d.bthhsFrSpcs1LevyQnt, "bthhsFrSpcs1LevyQnt"), + bthhsFrSpcs1UntprcAmt: formatNumber(d.bthhsFrSpcs1UntprcAmt, "bthhsFrSpcs1UntprcAmt"), + bthhsFrSpcs2LevyAmt: formatNumber(d.bthhsFrSpcs2LevyAmt, "bthhsFrSpcs2LevyAmt"), + bthhsFrSpcs2LevyQnt: formatNumber(d.bthhsFrSpcs2LevyQnt, "bthhsFrSpcs2LevyQnt"), + bthhsFrSpcs2UntprcAmt: formatNumber(d.bthhsFrSpcs2UntprcAmt, "bthhsFrSpcs2UntprcAmt"), + induLevyAmt: formatNumber(d.induLevyAmt, "induLevyAmt"), + induLevyQnt: formatNumber(d.induLevyQnt, "induLevyQnt"), + induUntprcAmt: formatNumber(d.induUntprcAmt, "induUntprcAmt"), + etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"), + etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"), + etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + levyQnt: formatNumber(d.levyQnt, "levyQnt"), + })); +} + +function buildCauserBurdenSaveList( + currentData: Record[], +): CauserBurdenSaveItem[] { + const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"), + mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"), + othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"), + othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"), + })); +} + // 하수도 통계 관리 > 09. 하수도재정 const SewerageFinanceInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -541,7 +706,144 @@ const SewerageFinanceInp = () => { }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("저장할 수 없는 탭입니다."); + return; + } + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (!(await confirm("저장 하시겠습니까?"))) return; + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("저장할 데이터가 없습니다."); + return; + } + + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (isSettleTab) { + const param: SewerageFinanceSettleSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageFinanceSettleSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageFinanceSettle(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isChargeTab) { + const param: SewerageChargeSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageChargeSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageCharge(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isUsageFeeTab) { + const param: SewerageUsageFeeSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageUsageFeeSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageUsageFee(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + const param: CauserBurdenSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildCauserBurdenSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = await statisticsInputSewerageFinanceApi.saveCauserBurden(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const openVerificationHistoryDialog = () => { diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts index a86546960..b44fad586 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts @@ -58,7 +58,7 @@ export const SewerageChargeInpColumn = { }, // 비고 - { name: 'rmrk', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts index f3e03d163..2cd90446b 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts @@ -59,37 +59,37 @@ export const SewerageUsageFeeInpColumn = { // 가정용 { name: 'hshusLevyAmt', fieldName: 'hshusLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'hshusLevyQnt', fieldName: 'hshusLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 업무용 { name: 'taskFrLevyAmt', fieldName: 'taskFrLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'taskFrLevyQnt', fieldName: 'taskFrLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 영업용(일반용) { name: 'fbuLevyAmt', fieldName: 'fbuLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'fbuLevyQnt', fieldName: 'fbuLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 욕탕1종(대중탕용) { name: 'bthhsFrSpcs1LevyAmt', fieldName: 'bthhsFrSpcs1LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'bthhsFrSpcs1LevyQnt', fieldName: 'bthhsFrSpcs1LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 욕탕2종 { name: 'bthhsFrSpcs2LevyAmt', fieldName: 'bthhsFrSpcs2LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'bthhsFrSpcs2LevyQnt', fieldName: 'bthhsFrSpcs2LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 산업 및 공업용 { name: 'induLevyAmt', fieldName: 'induLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'induLevyQnt', fieldName: 'induLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 기타 { name: 'etcLevyAmt', fieldName: 'etcLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { name: 'etcLevyQnt', fieldName: 'etcLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 비고 { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts index 0b6a919f0..8f1d97e13 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts @@ -5,12 +5,20 @@ import { ENDPOINT } from "@/shared/config"; import { type CauserBurdenApiResponse, type CauserBurdenRequestParams, + type CauserBurdenSaveRequest, + type CauserBurdenSaveResponse, type SewerageChargeApiResponse, type SewerageChargeRequestParams, + type SewerageChargeSaveRequest, + type SewerageChargeSaveResponse, type SewerageFinanceSettleApiResponse, type SewerageFinanceSettleRequestParams, + type SewerageFinanceSettleSaveRequest, + type SewerageFinanceSettleSaveResponse, type SewerageUsageFeeApiResponse, type SewerageUsageFeeRequestParams, + type SewerageUsageFeeSaveRequest, + type SewerageUsageFeeSaveResponse, type StatisticsInputSewerageFinanceService, } from "../../model/input/statistics-input-sewerage-finance-interface"; @@ -30,6 +38,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도재정 결산 저장 — ST00090001 */ + public async saveSewerageFinanceSettle( + request: SewerageFinanceSettleSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/settle/save`, + request, + ); + return response.data; + } + /** 하수도 요금 조회 — ST00090002 */ public async getSewerageChargeList( params: SewerageChargeRequestParams, @@ -41,6 +60,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도 요금 저장 — ST00090002 */ + public async saveSewerageCharge( + request: SewerageChargeSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/charge/save`, + request, + ); + return response.data; + } + /** 하수도 사용료 조회 — ST00090003 */ public async getSewerageUsageFeeList( params: SewerageUsageFeeRequestParams, @@ -52,6 +82,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도 사용료 저장 — ST00090003 */ + public async saveSewerageUsageFee( + request: SewerageUsageFeeSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/usage-fee/save`, + request, + ); + return response.data; + } + /** 원인자 부담금 조회 — ST00090004 */ public async getCauserBurdenList( params: CauserBurdenRequestParams, @@ -62,6 +103,17 @@ export class StatisticsInputSewerageFinanceApiService ); return response.data; } + + /** 원인자 부담금 저장 — ST00090004 */ + public async saveCauserBurden( + request: CauserBurdenSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/causer-burden/save`, + request, + ); + return response.data; + } } export const statisticsInputSewerageFinanceApi = diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts index d2928353f..e56a52858 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -86,6 +86,63 @@ export interface SewerageFinanceSettleRequestParams { mode?: string; } +/** + * ST00090001 저장 API `saveList[]` 한 행. + * `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageFinanceSettleSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; + fnnByWholPjtcoAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + wholSwagePplAmt: number; + wholSwageImpdFcltAmt: number; + etcWholAmt: number; +} + +/** ST00090001 저장 API 요청 */ +export interface SewerageFinanceSettleSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageFinanceSettleSaveItem[]; +} + +export interface SewerageFinanceSettleSaveResponse { + status: string; + message?: string; + data?: string; +} + /** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */ export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const; @@ -135,6 +192,40 @@ export interface SewerageChargeRequestParams { mode?: string; } +/** + * ST00090002 저장 API `saveList[]` 한 행. + * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageChargeSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + anlLevyQnt: number; + levyAmt: number; + grssPrmcoAmt: number; + rmrk: string | null; + avgUntprcAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; +} + +/** ST00090002 저장 API 요청 */ +export interface SewerageChargeSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageChargeSaveItem[]; +} + +export interface SewerageChargeSaveResponse { + status: string; + message?: string; + data?: string; +} + /** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */ export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const; @@ -201,6 +292,57 @@ export interface SewerageUsageFeeRequestParams { mode?: string; } +/** + * ST00090003 저장 API `saveList[]` 한 행. + * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageUsageFeeSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + fbuLevyAmt: number; + fbuLevyQnt: number; + fbuUntprcAmt: number; + bthhsFrSpcs1LevyAmt: number; + bthhsFrSpcs1LevyQnt: number; + bthhsFrSpcs1UntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induLevyAmt: number; + induLevyQnt: number; + induUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; + rmrk: string | null; + levyAmt: number; + levyQnt: number; +} + +/** ST00090003 저장 API 요청 */ +export interface SewerageUsageFeeSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageUsageFeeSaveItem[]; +} + +export interface SewerageUsageFeeSaveResponse { + status: string; + message?: string; + data?: string; +} + /** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */ export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const; @@ -252,6 +394,41 @@ export interface CauserBurdenRequestParams { mode?: string; } +/** + * ST00090004 저장 API `saveList[]` 한 행. + * `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface CauserBurdenSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyEvntcsCnt: number; + levyAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; + trgtByWholClctnAmt: number; +} + +/** ST00090004 저장 API 요청 */ +export interface CauserBurdenSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: CauserBurdenSaveItem[]; +} + +export interface CauserBurdenSaveResponse { + status: string; + message?: string; + data?: string; +} + /** 하수도재정 조회 공통 요청 파라미터 */ export interface SewerageFinanceRequestParams { ctpvNm?: string; @@ -265,13 +442,25 @@ export interface StatisticsInputSewerageFinanceService { getSewerageFinanceSettleList( params: SewerageFinanceSettleRequestParams, ): Promise; + saveSewerageFinanceSettle( + request: SewerageFinanceSettleSaveRequest, + ): Promise; getSewerageChargeList( params: SewerageChargeRequestParams, ): Promise; + saveSewerageCharge( + request: SewerageChargeSaveRequest, + ): Promise; getSewerageUsageFeeList( params: SewerageUsageFeeRequestParams, ): Promise; + saveSewerageUsageFee( + request: SewerageUsageFeeSaveRequest, + ): Promise; getCauserBurdenList( params: CauserBurdenRequestParams, ): Promise; + saveCauserBurden( + request: CauserBurdenSaveRequest, + ): Promise; } diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 721bb3ff3..60db9d6be 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -8,6 +8,11 @@ import { SkeletonPage } from "@/shared/ui/skeleton-page"; import "@rsone/components/index.css"; import Dashboard from "@/components/statistics/Dashboard"; +import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu"; +import { + parseStatisticsMenuUrlPath, + resolveStatisticsCategory, +} from "@/shared/lib/statistics-menu-url"; import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { useAdminStore } from "@/store/admin-store"; import { useTabStore } from "@/store/tab-store"; @@ -18,7 +23,9 @@ const StatisticsPage = () => { const tabsForBody = useMemo( () => tabs.filter( - (tab) => !isVerificationSelectionReportMenu(tab.menuItem), + (tab) => + !isVerificationSelectionReportMenu(tab.menuItem) && + !isBbsNoticeMenu(tab.menuItem), ), [tabs], ); @@ -57,13 +64,17 @@ const StatisticsPage = () => { {tabsForBody.map((tab) => { // 렌더링 시점에 컴포넌트가 없으면 바로 로드 if (!componentCache.current.has(tab.id)) { - const componentName = tab.urlPath; - const parentComponentName = tab.parentEngNm; + const { componentName } = parseStatisticsMenuUrlPath(tab.urlPath); + const category = resolveStatisticsCategory( + tab.urlPath, + tab.parentEngNm, + ); + const resolvedComponentName = componentName || tab.urlPath; const DynamicComponent = dynamic( () => import( - `@/components/statistics${parentComponentName ? `/${parentComponentName}` : ""}/${componentName}` + `@/components/statistics/${category}/${resolvedComponentName}` ), { ssr: false, loading: () => }, ); diff --git a/apps/st/src/shared/lib/bbs-notice-menu.ts b/apps/st/src/shared/lib/bbs-notice-menu.ts new file mode 100644 index 000000000..b0f7cef27 --- /dev/null +++ b/apps/st/src/shared/lib/bbs-notice-menu.ts @@ -0,0 +1,12 @@ +import type { MenuItem } from "@/entities/menu"; + +export const BBS_NOTICE_MENU_MARKER = "bbsnotice"; + +export function isBbsNoticeMenu(item: MenuItem): boolean { + const marker = BBS_NOTICE_MENU_MARKER; + return ( + item.urlPath?.toLowerCase().includes(marker) || + item.menuEngNm?.toLowerCase().includes(marker) || + item.menuPath?.toLowerCase().includes(marker) + ); +} diff --git a/apps/st/src/shared/lib/statistics-menu-url.ts b/apps/st/src/shared/lib/statistics-menu-url.ts new file mode 100644 index 000000000..fc9e74356 --- /dev/null +++ b/apps/st/src/shared/lib/statistics-menu-url.ts @@ -0,0 +1,73 @@ +import type { MenuItem } from "@/entities/menu"; + +export type ParsedStatisticsMenuUrl = { + categoryFromPath: string | null; + componentName: string | null; + rawUrlPath: string; +}; + +/** 메뉴 `urlPath`에서 통계 화면 카테고리·컴포넌트명을 파싱합니다. */ +export function parseStatisticsMenuUrlPath(urlPath: string): ParsedStatisticsMenuUrl { + const trimmed = (urlPath ?? "").trim(); + if (!trimmed) { + return { categoryFromPath: null, componentName: null, rawUrlPath: trimmed }; + } + + let pathPart = trimmed; + let queryString = ""; + const queryIndex = trimmed.indexOf("?"); + if (queryIndex >= 0) { + pathPart = trimmed.slice(0, queryIndex); + queryString = trimmed.slice(queryIndex + 1); + } + + const tabParam = new URLSearchParams(queryString).get("tab")?.trim() || null; + const segments = pathPart.replace(/^\/+/, "").split("/").filter(Boolean); + + let categoryFromPath: string | null = null; + if (segments[0] === "statistics" && segments.length >= 2) { + categoryFromPath = segments[1] ?? null; + } + + let componentName = tabParam; + if (!componentName) { + if (!pathPart.includes("/") && !queryString) { + componentName = trimmed; + } else if (segments.length === 1 && segments[0] !== "statistics") { + componentName = segments[0] ?? null; + } else if (segments[0] === "statistics" && segments.length >= 3) { + componentName = segments[segments.length - 1] ?? null; + } + } + + return { categoryFromPath, componentName, rawUrlPath: trimmed }; +} + +/** 동적 import용 카테고리 폴더명 (`input`, `review` 등) */ +export function resolveStatisticsCategory(urlPath: string, parentEngNm: string): string { + const { categoryFromPath } = parseStatisticsMenuUrlPath(urlPath); + return categoryFromPath || parentEngNm || ""; +} + +/** 탭 ID·URL `?tab=` 값 — 컴포넌트 파일명(확장자 제외) */ +export function resolveStatisticsTabId(urlPath: string): string { + const { componentName, rawUrlPath } = parseStatisticsMenuUrlPath(urlPath); + return componentName || rawUrlPath; +} + +/** `@/components/statistics/{category}/{Component}` 경로 */ +export function buildStatisticsComponentImportPath( + urlPath: string, + parentEngNm: string, +): string { + const { componentName } = parseStatisticsMenuUrlPath(urlPath); + const category = resolveStatisticsCategory(urlPath, parentEngNm); + const name = componentName || urlPath; + return `@/components/statistics/${category}/${name}`; +} + +export function menuMatchesStatisticsTabId(menu: MenuItem, tabId: string): boolean { + if (!tabId) return false; + if (menu.urlPath === tabId) return true; + return resolveStatisticsTabId(menu.urlPath) === tabId; +} diff --git a/apps/st/src/shared/lib/stats-cd-input-navigation.ts b/apps/st/src/shared/lib/stats-cd-input-navigation.ts index 6a00d33ae..56b8c791d 100644 --- a/apps/st/src/shared/lib/stats-cd-input-navigation.ts +++ b/apps/st/src/shared/lib/stats-cd-input-navigation.ts @@ -1,6 +1,7 @@ import type { MenuItem } from "@/entities/menu"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { doNameList } from "@/components/util/column/input/DoList"; +import { menuMatchesStatisticsTabId } from "@/shared/lib/statistics-menu-url"; import { SEWAGE_PIPE_INP_TAB_KEYS, SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, @@ -97,7 +98,7 @@ export function findMenuItemByUrlPath( urlPath: string, ): MenuItem | null { for (const item of menus) { - if (item.urlPath === urlPath) return item; + if (menuMatchesStatisticsTabId(item, urlPath)) return item; if (item.children?.length) { const found = findMenuItemByUrlPath(item.children, urlPath); if (found) return found; diff --git a/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts b/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts index 508e6e834..9388438db 100644 --- a/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts +++ b/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts @@ -4,6 +4,7 @@ import { useEffect, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import type { MenuItem } from "@/entities/menu"; +import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu"; import { findMenuItemByUrlPath } from "@/shared/lib/stats-cd-input-navigation"; import { getTabStoreState } from "@/store/tab-store"; @@ -61,7 +62,7 @@ export function useStatisticsTabUrlSync({ if (getTabStoreState().activeTabId === tabParam) return; const menu = findMenuItemByUrlPath(menuList, tabParam); - if (!menu) return; + if (!menu || isBbsNoticeMenu(menu)) return; setCurrentMenuInfo(menu); const ok = addTab(menu); diff --git a/apps/st/src/shared/ui/statistics-page-layout.tsx b/apps/st/src/shared/ui/statistics-page-layout.tsx index 857f8441e..26a9d6ddc 100644 --- a/apps/st/src/shared/ui/statistics-page-layout.tsx +++ b/apps/st/src/shared/ui/statistics-page-layout.tsx @@ -5,7 +5,7 @@ import { TabBar } from "./tab-bar"; function StatisticsPageLayout({ children }: { children: React.ReactNode }) { const { activeTabId, tabs } = useTabStore(); - const pageTitle = tabs.find((t) => t.menuItem.urlPath === activeTabId); + const pageTitle = tabs.find((t) => t.id === activeTabId); const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim()); diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 6909ed26b..3a75f37aa 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -4,6 +4,8 @@ import { createJSONStorage, persist } from "zustand/middleware"; import type { MenuItem } from "@/entities/menu"; import { MenuAuthorityApi } from "@/entities/menu-authority"; import type { PageMenuAuthority } from "@/entities/menu-authority/model/menu-authority-interface"; +import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu"; +import { resolveStatisticsTabId } from "@/shared/lib/statistics-menu-url"; export interface Tab { id: string; // 고유 ID (urlPath) @@ -101,11 +103,18 @@ function tabForPersist(tab: Tab): Tab { }; } +function normalizePersistedTab(tab: Tab): Tab { + const tabId = resolveStatisticsTabId(tab.urlPath || tab.id); + return tabId === tab.id ? tab : { ...tab, id: tabId }; +} + function sanitizePersistedTabsAndActive( tabs: Tab[], activeTabId: string | null, ): { tabs: Tab[]; activeTabId: string | null } { - let nextTabs = Array.isArray(tabs) ? tabs : []; + let nextTabs = Array.isArray(tabs) + ? tabs.filter((tab) => !isBbsNoticeMenu(tab.menuItem)).map(normalizePersistedTab) + : []; if (nextTabs.length > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS); if (nextTabs.length === 0) { @@ -130,8 +139,12 @@ const useTabStoreHook = create()( activeTabId: null, menuList: null, addTab: (menuItem: MenuItem) => { + if (isBbsNoticeMenu(menuItem)) { + return false; + } + const { tabs, menuList } = get(); - const tabId = menuItem.urlPath || ""; + const tabId = resolveStatisticsTabId(menuItem.urlPath || ""); const existingTab = tabs.find((tab) => tab.id === tabId); From 0eb74b46ba190e386c666ab9f9e7463901d5d10e Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 15 Jun 2026 16:42:38 +0900 Subject: [PATCH 15/15] =?UTF-8?q?[st]=20feat:=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=97=90=20'/cms'=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=EC=8B=9C=20=ED=83=AD=20=EB=A1=9C=EC=A7=81=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/(with-base-layout)/layout.tsx | 173 +----------------- 1 file changed, 4 insertions(+), 169 deletions(-) diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index e849c3a1d..1f7ad2fa3 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -1,54 +1,27 @@ "use client"; - - import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; - import { usePathname } from "next/navigation"; - - import { - getTabStoreState as getGridTabStoreState, - useTabStore as gridTabStore, - } from "@rsone/components"; - import { useQuery, useQueryClient } from "@tanstack/react-query"; - - import { ENDPOINT } from "@/shared/config"; - import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; - import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync"; - import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; - import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; - import { tokenManager } from "@/entities/auth"; - import type { MenuItem } from "@/entities/menu"; - import { MenuApi } from "@/entities/menu"; - import { BaseLayout } from "@common/components"; - import { getConfig } from "@common/config"; - - -import { getTabStoreState, useTabStore } from "@/store/tab-store"; - - - import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport"; - - +import { getTabStoreState, useTabStore } from "@/store/tab-store"; const appConfig = getConfig(); @@ -58,42 +31,22 @@ const appSiteId = getCookie("site-id-key") ?? ""; const isLoggedIn = !!userId; - - -function WithBaseLayoutInner({ - - children, - -}: { - - children: React.ReactNode; - -}) { - +function WithBaseLayoutInner({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const queryClient = useQueryClient(); - - /** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */ const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] = - useState(false); - - const setCurrentMenuInfo = useMenuStore( - (state) => state.setCurrentMenuInfo, - ); const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); - - const addTab = useTabStore((state) => state.addTab); const setActiveTab = useTabStore((state) => state.setActiveTab); @@ -108,16 +61,11 @@ function WithBaseLayoutInner({ const addTabForGrid = gridTabStore((state) => state.addTab); - const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab); - const setMenuListForGrid = gridTabStore((state) => state.setMenuList); const alert = useAlertStore((state) => state.setMessage); - - const { syncTabToUrl } = useStatisticsTabUrlSync({ - pathname, menuList, @@ -129,41 +77,27 @@ function WithBaseLayoutInner({ addTabForGrid, setCurrentMenuInfo, - }); - - const { data: menuListData } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], queryFn: () => MenuApi.getMenuRoles(appSiteId), enabled: isLoggedIn, - }); - - useEffect(() => { - if (menuListData) { - setMenuList(menuListData); setMenuListForGrid(menuListData); - } - }, [menuListData, setMenuList, setMenuListForGrid]); - - // 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤 useEffect(() => { - if (!menuList) return; const appTabs = getTabStoreState().tabs; @@ -172,20 +106,13 @@ function WithBaseLayoutInner({ if (getGridTabStoreState().tabs.length > 0) return; - - for (const tab of appTabs) { - getGridTabStoreState().addTab(tab.menuItem); - } - - const activeId = getTabStoreState().activeTabId; if (activeId) { - setActiveTab(activeId); const activeTab = appTabs.find((t) => t.id === activeId); @@ -193,155 +120,97 @@ function WithBaseLayoutInner({ const gridActiveId = activeTab?.menuItem.menuEngNm; if (gridActiveId) { - getGridTabStoreState().setActiveTab(gridActiveId); - } - } - }, [menuList, tabs, setActiveTab]); - - // 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거 const currentPath = useMemo(() => { - if (tabs.length === 0) return ""; - - if (activeTabId && menuList) { - // menuList에서 활성 탭의 메뉴 찾기 const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => { - for (const item of items) { - if (item.urlPath === activeTabId) { - return item; - } if (item.children) { - const found = findMenuByEngNm(item.children); if (found) return found; - } - } return null; - }; - - const activeMenu = findMenuByEngNm(menuList); - - if (activeMenu?.urlPath) { - return activeMenu.urlPath; - } - } - - // 탭에서 직접 찾기 if (activeTabId) { - const currentTabs = getTabStoreState().tabs; const activeTab = currentTabs.find((t) => t.id === activeTabId); if (activeTab?.menuItem?.urlPath) { - return activeTab.menuItem.urlPath; - } - } - - // currentMenuInfo에서 가져오기 if (currentMenuInfo?.urlPath) { - return currentMenuInfo.urlPath; - } - - // 기본값으로 pathname 사용 return pathname ?? ""; - }, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]); - - const handleMenuClick = useCallback( - (item: MenuItem) => { + if (item.urlPath.includes("/cms/")) return; setCurrentMenuInfo(item); - - if (isVerificationSelectionReportMenu(item)) { - setReportPanelOpen(true); return; - } - - const addTabResult: boolean = addTab(item); - - // packages/rsone의 스토어에도 동기화 if (addTabResult) { - addTabForGrid(item); if (item.urlPath) { - syncTabToUrl(item.urlPath); - } - } - - if (!addTabResult) { - alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다. 기존 화면을 닫은 후 이용해주세요.`); - } - }, [ - setCurrentMenuInfo, addTab, @@ -353,71 +222,37 @@ function WithBaseLayoutInner({ setReportPanelOpen, syncTabToUrl, - ], - ); - - return ( - - -
- +
{isVerificationSelectionReportPanelOpen && ( - setReportPanelOpen(false)} - /> - )} {children} -
- - ); - } - - export default function WithBaseLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - -