diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index d90874c8c..d4b5429f8 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -8,6 +8,9 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly { @apply !bg-[#f0f0f0] !text-[#666]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error { @apply !bg-[#ffe6e6] !text-[#c00]; } @@ -153,6 +156,15 @@ .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other { @apply !bg-[#fff7bf]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr, + .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; + } + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-curr, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr, + .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; + } /* * 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용. diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 957318ab9..e51214178 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; +import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react"; import JSZip from "jszip"; import { ValueType } from "realgrid"; @@ -14,6 +14,7 @@ import type { CellStyleCallbackParams, CellStyleCallbackResult, CellTooltipCallback, + ColumnConfig, ColumnLayoutItem, DataGridProps, FooterItemWithCallback, @@ -48,6 +49,63 @@ function mergeCellStyleCallbackResult( }; } +type GridColumnWithStyleCallback = { + styleCallback?: ( + grid: unknown, + dataCell: { + itemIndex?: number; + index?: { itemIndex?: number }; + dataField?: string; + fieldName?: string; + }, + ) => CellStyleCallbackResult; +}; + +function applyCellStyleCallbacksToColumns( + gridView: unknown, + columns: ColumnConfig[], + rowDataRef: MutableRefObject[]>, + cellStyleCallbackRef: MutableRefObject, +) { + if (!cellStyleCallbackRef.current || !gridView) return; + + const gv = gridView as { + columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined; + }; + if (typeof gv.columnByName !== "function") return; + + columns.forEach((column) => { + const baseStyleName = column.styleName; + let gridCol: GridColumnWithStyleCallback | null | undefined; + try { + // columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출 + gridCol = gv.columnByName!(column.name); + } catch { + return; + } + if (!gridCol) return; + + gridCol.styleCallback = (grid, dataCell) => { + const itemIndex = + dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; + const fieldName = + dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName; + const data = rowDataRef.current; + if (itemIndex < 0 || itemIndex >= data.length) { + return mergeCellStyleCallbackResult(baseStyleName, undefined); + } + const row = data[itemIndex] as Record; + const extra = cellStyleCallbackRef.current?.({ + rowIndex: itemIndex, + fieldName, + row, + grid: grid as CellStyleCallbackParams["grid"], + }); + return mergeCellStyleCallbackResult(baseStyleName, extra); + }; + }); +} + // RealGrid 타입 확장 declare module "realgrid" { interface GridView { @@ -725,9 +783,16 @@ const DataGrid = ({ }, [highlightedRowId, rowData, idFieldName]); useLayoutEffect(() => { - if (!cellStyleCallback || !gridRef.current?.gridView) return; - gridRef.current.gridView.refresh(); - }, [cellStyleCallback, rowData]); + const gv = gridRef.current?.gridView; + if (!gv || !cellStyleCallback) return; + applyCellStyleCallbacksToColumns( + gv, + columns, + rowDataRef, + cellStyleCallbackRef, + ); + (gv as { refresh?: () => void }).refresh?.(); + }, [cellStyleCallback, columns, rowData]); const { setMessage: alert } = useAlertStore((state) => state); const activeTabId = useTabStore((state) => state.activeTabId); @@ -1049,6 +1114,10 @@ const DataGrid = ({ const gridView = gridRef.current.gridView; const dataProvider = gridRef.current.dataProvider; + const footerTimerIds: ReturnType[] = []; + const scheduleFooterTask = (task: () => void, delayMs: number) => { + footerTimerIds.push(setTimeout(task, delayMs)); + }; applyDefaultGridEditOptions(gridView as GridViewWithEditOptions); const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001"); @@ -1189,8 +1258,10 @@ const DataGrid = ({ }); const applyFooterSettings = ( - targetGridView: typeof gridView, + targetGridView: typeof gridView | null | undefined, ) => { + if (!targetGridView?.setOptions) return; + const hasFooter = columns.some((col) => col.footer); if (!hasFooter) return; @@ -1308,7 +1379,7 @@ const DataGrid = ({ }; // setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함 - setTimeout(() => { + scheduleFooterTask(() => { if (gridRef.current?.gridView && columnLayout) { const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]); if (safeLayout.length === 0) return; @@ -1327,10 +1398,9 @@ const DataGrid = ({ // footer 설정 (배열 또는 단일 객체 모두 처리) // setFooters 호출 후 footer 배열 설정 - setTimeout(() => { + scheduleFooterTask(() => { const currentGridView = gridRef.current?.gridView; - if (!currentGridView || !currentGridView.columnByName) { - console.warn("GridView not available for footer setup"); + if (!currentGridView?.columnByName) { return; } @@ -1385,19 +1455,28 @@ const DataGrid = ({ col, ), ); + applyCellStyleCallbacksToColumns( + currentGridView, + columns, + rowDataRef, + cellStyleCallbackRef, + ); applyFooterSettings(currentGridView); if (hasFooterUserSpans) { currentGridView.setColumnLayout?.(columnLayout); } // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 - setTimeout(() => { + scheduleFooterTask(() => { + const liveGridView = gridRef.current?.gridView; + if (!liveGridView?.columnByName) return; + columns.forEach((col) => { if (!col.footer || !Array.isArray(col.footer)) return; try { - const column = currentGridView.columnByName( + const column = liveGridView.columnByName( col.name, ); if (!column) return; @@ -1429,11 +1508,17 @@ const DataGrid = ({ ); } }); - applyFooterSettings(currentGridView); + applyCellStyleCallbacksToColumns( + liveGridView, + columns, + rowDataRef, + cellStyleCallbackRef, + ); + applyFooterSettings(liveGridView); if (hasFooterUserSpans) { - currentGridView.setColumnLayout?.(columnLayout); + liveGridView.setColumnLayout?.(columnLayout); } - currentGridView.refresh?.(); + liveGridView.refresh?.(); }, 50); } catch (e) { console.warn("Failed to set columns with footer array:", e); @@ -1495,6 +1580,7 @@ const DataGrid = ({ } return () => { + footerTimerIds.forEach(clearTimeout); if (getGridDataRef) getGridDataRef.current = null; }; }, [ @@ -1939,19 +2025,9 @@ const DataGrid = ({ }; } columns.forEach((column) => { - const baseStyleName = column.styleName; const gridCol = gv.columnByName( column.name, ) as { - styleCallback?: ( - g: unknown, - dataCell: { - itemIndex?: number; - index?: { itemIndex?: number }; - dataField?: string; - fieldName?: string; - }, - ) => CellStyleCallbackResult; renderer?: { showTooltip?: boolean }; } | null; if (gridCol) { @@ -1990,43 +2066,14 @@ const DataGrid = ({ { showTooltip: true }, ); } - gridCol.styleCallback = (grid, dataCell) => { - const itemIndex = - dataCell?.index?.itemIndex ?? - dataCell?.itemIndex ?? - -1; - const fieldName = - dataCell?.dataField ?? - dataCell?.fieldName ?? - column.fieldName; - const data = rowDataRef.current; - if ( - itemIndex < 0 || - itemIndex >= data.length - ) { - return mergeCellStyleCallbackResult( - baseStyleName, - undefined, - ); - } - const row = data[itemIndex] as Record< - string, - string | number - >; - const extra = - cellStyleCallbackRef.current?.({ - rowIndex: itemIndex, - fieldName, - row, - grid: grid as CellStyleCallbackParams["grid"], - }); - return mergeCellStyleCallbackResult( - baseStyleName, - extra, - ); - }; } }); + applyCellStyleCallbacksToColumns( + gv, + columns, + rowDataRef, + cellStyleCallbackRef, + ); gv.refresh(); }, 0); }} diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index bde68dc26..863a82226 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -234,6 +234,7 @@ export function VerificationResultHistoryDialog({ }; const checkedVrfcByTabRef = useRef(checkedVrfcByTab); checkedVrfcByTabRef.current = checkedVrfcByTab; + const loadedTabKeysRef = useRef>(new Set()); const resetState = useCallback(() => { setActiveTab("NV"); @@ -243,6 +244,7 @@ export function VerificationResultHistoryDialog({ setErrorMessage(null); setResetSubmitting(false); setCheckedVrfcByTab(createEmptyCheckedVrfcByTab()); + loadedTabKeysRef.current.clear(); }, []); const fetchVerificationResultPage = useCallback( @@ -279,10 +281,10 @@ export function VerificationResultHistoryDialog({ ); const loadTabPage = useCallback( - async (tab: VerificationHistoryTabKey, pageIndex: number) => { + async (tab: VerificationHistoryTabKey, pageIndex: number): Promise => { if (!stableQueryContext) { setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); - return; + return false; } setErrorMessage(null); setLoadingByTab((prev) => ({ ...prev, [tab]: true })); @@ -296,10 +298,12 @@ export function VerificationResultHistoryDialog({ ? Math.max(0, Number(apiCurrentPage) - 1) : pageIndex; applyPageResponse(tab, resolvedPageIndex, list, totalPages); + return true; } catch { const tabLabel = verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab; setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`); + return false; } finally { setLoadingByTab((prev) => ({ ...prev, [tab]: false })); } @@ -314,40 +318,27 @@ export function VerificationResultHistoryDialog({ } setErrorMessage(null); setTabPages(initialTabPageState()); - setLoadingByTab({ NV: true, IMR: true, HB: true }); - try { - const results = await Promise.all( - verificationHistoryTabs.map((tab) => - fetchVerificationResultPage(tab.value, 0), - ), - ); - setEntriesByTab({ - NV: results[0]?.data?.verifyResultList ?? [], - IMR: results[1]?.data?.verifyResultList ?? [], - HB: results[2]?.data?.verifyResultList ?? [], - }); - setTabPages({ - NV: { - pageIndex: 0, - pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - IMR: { - pageIndex: 0, - pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - HB: { - pageIndex: 0, - pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - }); - } catch { - setEntriesByTab({ NV: [], IMR: [], HB: [] }); - setTabPages(initialTabPageState()); - setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다."); - } finally { - setLoadingByTab({ NV: false, IMR: false, HB: false }); + setEntriesByTab({ NV: [], IMR: [], HB: [] }); + loadedTabKeysRef.current.clear(); + const loaded = await loadTabPage("NV", 0); + if (loaded) { + loadedTabKeysRef.current.add("NV"); } - }, [fetchVerificationResultPage, stableQueryContext]); + }, [loadTabPage, stableQueryContext]); + + const handleActiveTabChange = useCallback( + (tab: VerificationHistoryTabKey) => { + setActiveTab(tab); + if (loadedTabKeysRef.current.has(tab)) return; + void (async () => { + const loaded = await loadTabPage(tab, 0); + if (loaded) { + loadedTabKeysRef.current.add(tab); + } + })(); + }, + [loadTabPage], + ); const handleTabPageChange = useCallback( (tab: VerificationHistoryTabKey, pageIndex: number) => { @@ -577,7 +568,7 @@ export function VerificationResultHistoryDialog({ )} setActiveTab(value as VerificationHistoryTabKey)} + onValueChange={(value) => handleActiveTabChange(value as VerificationHistoryTabKey)} className="w-full" variant="default" > diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index d0c73759e..3bcb3364b 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -582,11 +582,6 @@ const PublicSewageFacilityInp = () => { })(); }; - const updateOperateDataButtonFunc = () => { - console.log('updatedOperateData'); - // 운영 데이터 업데이트 로직 - }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [activeTab, setActiveTab] = useState( PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001', @@ -766,6 +761,52 @@ const PublicSewageFacilityInp = () => { } }; + const updateOperateDataButtonFunc = () => { + void (async () => { + if (!lastSearchedValues) { + alert('먼저 조회를 진행해주세요.'); + return; + } + if (!(await confirm('운영 데이터를 업데이트 하시겠습니까?'))) return; + + try { + showGlobalProgress({ label: '운영 데이터 업데이트 중...', progress: 0 }); + const userInfo = await getUserInfo(); + const request = { + statsYr: lastSearchedValues.year ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + userId: userInfo.userUnqId, + }; + + const response = + await statisticsInputPublicSewageFacilityApiService.updatePublicSewageFacilityOperationData( + request, + ); + if (response.status !== 'success') { + alert(response.message || '운영 데이터 업데이트 중 오류가 발생했습니다.'); + return; + } + + setGlobalProgress(50); + const param: PublicSewageFacilityRequestParams = { + statsYr: request.statsYr, + ctpvNm: request.ctpvNm, + sggNm: request.sggNm, + sewerBzmnCd: request.sewerBzmnCd, + }; + await loadPublicSewageFacilityActiveTabData('ST00060001', param, lastSearchedValues); + setGlobalProgress(100); + alert('운영 데이터가 업데이트되었습니다.'); + } catch { + alert('운영 데이터 업데이트 중 오류가 발생했습니다.'); + } finally { + hideGlobalProgress(); + } + })(); + }; + const runMainFacilityMappingSave = async ( draft: MainFacilityMappingSaveDraft, dialogInput: MainFacilityMappingSaveDialogValues | null, diff --git a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx index 902fea894..5a5519dee 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx @@ -660,7 +660,7 @@ const SewageExcretaSlurgeInp = () => { Map >(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); - const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< Map >(new Map()); const [sggOptions, setSggOptions] = useState([]); diff --git a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx index 5bba59ad1..29fb142d3 100644 --- a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx @@ -50,7 +50,7 @@ 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 { ColumnInfo } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { doList } from "@/components/util/column/input/DoList"; import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn"; diff --git a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx index bbca7cf5c..15aafdd95 100644 --- a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx +++ b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx @@ -4,7 +4,6 @@ import { useMemo, useRef, useState, - type RefObject, type Dispatch, type SetStateAction, } from "react"; @@ -25,7 +24,7 @@ import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButt 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 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"; @@ -200,20 +199,6 @@ function createEmptyRainfallPumpStationRow( return row; } -function applySggNmDropDownToReservoirGrid( - gridRef: RefObject, - labels: string[], -) { - if (labels.length === 0) return; - applyDropDownColumnToGrid( - gridRef.current?.gridView as { - setColumnProperty?: (col: string, key: string, value: unknown) => void; - }, - "sggNm", - labels, - ); -} - function parseSggListFromResponse(res: ApiResponse): Sgg[] { if (res.status !== "success" || res.data == null) return []; return Array.isArray(res.data) ? res.data : []; @@ -283,6 +268,16 @@ function toGridNumber(value: string | number | null | undefined): number { return Number.isFinite(n) ? n : 0; } +/** 펌프 구경(mm) — 콤마 제거 후 숫자 변환, 소수점 버림 */ +function toPumpDmtrExpln(value: string | number | null | undefined): number { + const raw = String(value ?? "").trim(); + if (raw === "") return 0; + const normalized = raw.replace(/,/g, ""); + const num = Number(normalized); + if (!Number.isFinite(num)) return 0; + return Math.floor(num); +} + /** 시군구명(`sggNm`) 기준 `admdstCd`(bsnmCd)·`admdstNm`(시군구명) */ function resolveSewageReservoirAdmdst( sggNm: string, @@ -322,15 +317,15 @@ function buildSewageReservoirSaveItem( wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), impdFcltCpct: toGridNumber(row.impdFcltCpct), grssPpqty: toGridNumber(row.grssPpqty), - pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""), + pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln), pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty), pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt), pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt), - pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""), + pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln), pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty), pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt), pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt), - pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""), + pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln), pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), @@ -375,15 +370,15 @@ function buildRainfallPumpStationSaveItem( rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt), abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt), grssPpqty: toGridNumber(row.grssPpqty), - pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""), + pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln), pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty), pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt), pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt), - pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""), + pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln), pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty), pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt), pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt), - pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""), + pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln), pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index a5726399a..2308dc948 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -850,8 +850,8 @@ const WaterReuseInp = () => { (typeof gridView.getCheckedItems === "function" ? gridView.getCheckedItems() : typeof gridView.getCheckedRows === "function" - ? gridView.getCheckedRows() - : []) ?? []; + ? gridView.getCheckedRows() + : []) ?? []; return checked.filter( (idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount, @@ -1166,8 +1166,8 @@ const WaterReuseInp = () => { const submitList = noApplicable ? [] : currentData.map((row) => - buildRecycledWaterStatusSaveItem(row, list), - ); + buildRecycledWaterStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => buildRecycledWaterStatusSaveItem(row, list)); @@ -1186,8 +1186,8 @@ const WaterReuseInp = () => { const submitList = noApplicable ? [] : currentData.map((row) => - buildRainFallUtilFactStatusSaveItem(row, list), - ); + buildRainFallUtilFactStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); @@ -1206,8 +1206,8 @@ const WaterReuseInp = () => { const submitList = noApplicable ? [] : currentData.map((row) => - buildTreatWaterReuseDesignConstStatusSaveItem(row, list), - ); + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => @@ -1231,8 +1231,8 @@ const WaterReuseInp = () => { const submitList = noApplicable ? [] : currentData.map((row) => - buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), - ); + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => @@ -1417,8 +1417,8 @@ const WaterReuseInp = () => { const saveList = noApplicable ? [] : currentData.map((row) => - buildRecycledWaterStatusSaveItem(row, list), - ); + buildRecycledWaterStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => buildRecycledWaterStatusSaveItem(row, list)); @@ -1477,8 +1477,8 @@ const WaterReuseInp = () => { const saveList = noApplicable ? [] : currentData.map((row) => - buildRainFallUtilFactStatusSaveItem(row, list), - ); + buildRainFallUtilFactStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); @@ -1538,8 +1538,8 @@ const WaterReuseInp = () => { const saveList = noApplicable ? [] : currentData.map((row) => - buildTreatWaterReuseDesignConstStatusSaveItem(row, list), - ); + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => @@ -1603,8 +1603,8 @@ const WaterReuseInp = () => { const saveList = noApplicable ? [] : currentData.map((row) => - buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), - ); + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); const deleteList = (deleteListByTab.get(tabKey) ?? []) .filter((row) => isPersistedArticleRow(tabKey, row)) .map((row) => @@ -1652,7 +1652,56 @@ const WaterReuseInp = () => { }; const updateOperateDataButtonFunc = () => { - alert("운영 데이터 업데이트 API 연동 준비 중입니다."); + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!(await confirm("운영 데이터를 업데이트 하시겠습니까?"))) return; + + try { + showGlobalProgress({ label: "운영 데이터 업데이트 중...", progress: 0 }); + const userInfo = await getUserInfo(); + const request = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + }; + + const response = + await statisticsInputWaterReuseApi.updateWaterReuseOperationData(request); + if (response.status !== "success") { + alert(response.message || "운영 데이터 업데이트 중 오류가 발생했습니다."); + return; + } + + setGlobalProgress(50); + const param: WaterReuseRequestParams = { + statsYr: request.statsYr, + ctpvNm: request.ctpvNm, + sggNm: request.sggNm, + sewerBzmnCd: request.sewerBzmnCd, + }; + await loadActiveTabData( + PublicSewageTreatWaterReuseInpColumn.tabKey, + param, + lastSearchedValues, + false, + false, + ); + setGlobalProgress(100); + alert("운영 데이터가 업데이트되었습니다."); + } catch { + alert("운영 데이터 업데이트 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + })(); }; const addArticleFunc = async () => { @@ -1716,10 +1765,10 @@ const WaterReuseInp = () => { checkedIndexes.length > 0 ? new Set(checkedIndexes) : new Set( - currentRows - .map((row, idx) => (isCheckedRow(row) ? idx : -1)) - .filter((idx) => idx >= 0), - ); + currentRows + .map((row, idx) => (isCheckedRow(row) ? idx : -1)) + .filter((idx) => idx >= 0), + ); const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx)); const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); @@ -1951,8 +2000,8 @@ const WaterReuseInp = () => { e.vrfcCd === "IMR" ? "IMR검증 - " : e.vrfcCd === "NV" - ? "일반검증 - " - : "HB검증 - "; + ? "일반검증 - " + : "HB검증 - "; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; }) .filter(Boolean) diff --git a/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx b/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx index 961f0a79e..d7bd7a122 100644 --- a/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx @@ -1,38 +1,315 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { LoadingWithAnimation } from "@common/components"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import type { + ExcretaTreatmentFacilityReviewItem, +} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; +import { + extractExcretaTreatmentFacilityReviewList, + extractExcretaTreatmentFacilityReviewStatsStatusInfo, +} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewOpinionSaveRequest, + ReviewerInfoRequest, +} from "@/entities/statistics/model/statistics-interface"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 07.분뇨처리 시설 +const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0]; + +// 하수도 통계 검토 07. 분뇨처리시설 const ExcretaTreatmentFacilityRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [statsStatusInfo, setStatsStatusInfo] = useState({}); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [reviewOpinionContext, setReviewOpinionContext] = + useState(null); + const [latestReviewOpinion, setLatestReviewOpinion] = useState(""); + const [latestProcessHistory, setLatestProcessHistory] = useState(""); + const [lastSearchValues, setLastSearchValues] = useState(null); - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const columnInfo = useMemo(() => { + if (!excretaTreatmentFacilityTab) { + return { columns: [], columnLayout: [], fixedColumns: 0, rowStartNum: 0 }; + } - const approveFunc = () => console.log('approveFunc'); - const saveFunc = () => console.log('submit'); - const reviewOpinionFunc = () => console.log('reviewOpinionFunc'); - const returnFunc = () => console.log('returnFunc'); + const { columns, columnLayout, fixedColumns, rowStartNum } = excretaTreatmentFacilityTab; + return { + columnLayout, + fixedColumns, + rowStartNum, + columns: columns.map((col) => ({ + ...col, + editable: false as const, + })), + }; + }, []); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [statsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const submitDialogContext = useMemo(() => { + if (reviewOpinionContext == null) { + return null; + } + + const instSeCd = userInfo?.instSeCd ?? ""; + if (!instSeCd) { + return null; + } + + return { + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + instSeCd, + }; + }, [reviewOpinionContext, userInfo?.instSeCd]); + + const handleReset = () => { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + setLastSearchValues(null); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + setLastSearchValues(values); + + try { + setIsLoading(true); + + const response = + await statisticsManagementApi.getExcretaTreatmentFacilityReviewList({ + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + swtrmtFcltNm: values.fctName, + }); + + if (response.status === "success" && response.data) { + const rawList = extractExcretaTreatmentFacilityReviewList(response.data); + const statsInfo = extractExcretaTreatmentFacilityReviewStatsStatusInfo(response.data); + const convertedData: ExcretaTreatmentFacilityReviewItem[] = rawList.map((item) => ({ + ...item, + admdstNm: item.admdstNm ?? "", + fcltPstnCn: item.fcltPstnCn ?? "", + telNo: item.telNo ?? "", + cmptnStrtYmd: item.cmptnStrtYmd ?? "", + oprtngStrtYmd: item.oprtngStrtYmd ?? "", + prcsMthdExpln: item.prcsMthdExpln ?? "", + linkTmplntCn: item.linkTmplntCn ?? "", + wtsysNm: item.wtsysNm ?? "", + trbtrNm: item.trbtrNm ?? "", + msirbCn: item.msirbCn ?? "", + ssirbCn: item.ssirbCn ?? "", + operMthdCdCn: item.operMthdCdCn ?? "", + rmrk: item.rmrk ?? "", + })); + + setStatsStatusInfo(statsInfo); + setReviewOpinionContext({ + statsYr: values.year ?? "", + sewerBzmnCd: values.bsnmCd ?? "", + statsCd: statsInfo.statsCd ?? "", + }); + setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? ""); + setLatestProcessHistory(statsInfo.wrterOpnnCn ?? ""); + setRowData(toGridRows(convertedData as unknown as object[])); + } else { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + alert(response.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setLatestReviewOpinion(row?.splmntOpnnCn ?? ""); + setLatestProcessHistory(row?.wrterOpnnCn ?? ""); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setStatsStatusInfo((prev) => ({ + ...prev, + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + })); + setLatestReviewOpinion(param.splmntOpnnCn); + setLatestProcessHistory(param.wrterOpnnCn); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && } +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ +
+ + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
- - ); }; -export default ExcretaTreatmentFacilityRvw; \ No newline at end of file +export default ExcretaTreatmentFacilityRvw; diff --git a/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx b/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx index 619085736..3d995127a 100644 --- a/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx @@ -1,51 +1,428 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractPrivateSewageFacilityReviewList, + extractPrivateSewageFacilityReviewStatsStatusInfo, + mapPrivateSewageFacilityReviewRows, + type PrivateSewageFacilityReviewRequestParams, +} from "@/entities/statistics/model/statistics-private-sewage-facility-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 08.개인하수처리시설 +const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001"; + +async function fetchPrivateSewageFacilityReviewTab( + tabKey: string, + searchParam: PrivateSewageFacilityReviewRequestParams, +) { + if (tabKey === "ST00080001") { + return statisticsManagementApi.getPrivateSewageTreatFactStatusReviewList(searchParam); + } + if (tabKey === "ST00080002") { + return statisticsManagementApi.getSepticTankStatusReviewList(searchParam); + } + if (tabKey === "ST00080003") { + return statisticsManagementApi.getPrivateSewageDesigStatusReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 08. 개인하수처리시설 const PrivateSewageFacilityRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return PrivateSewageFacilityInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: PrivateSewageFacilityReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); + + const response = await fetchPrivateSewageFacilityReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractPrivateSewageFacilityReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapPrivateSewageFacilityReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchPrivateSewageFacilityReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractPrivateSewageFacilityReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapPrivateSewageFacilityReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; diff --git a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx index 6d23f3c87..837197c75 100644 --- a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx @@ -22,7 +22,7 @@ import { type ReviewOpinionDialogContext, } from "@/components/statistics/review/ReviewOpinionDialog"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn"; @@ -250,6 +250,7 @@ const PublicSewageFacilityRvw = () => { rowStartNum={columnInfo.rowStartNum} rowData={rowData} setRowData={setRowData} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} /> { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowData, setRowData] = useState[]>([]); const [isLoading, setIsLoading] = useState(false); + const [statsStatusInfo, setStatsStatusInfo] = useState({}); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [reviewOpinionContext, setReviewOpinionContext] = + useState(null); + const [latestReviewOpinion, setLatestReviewOpinion] = useState(""); + const [latestProcessHistory, setLatestProcessHistory] = useState(""); + const [lastSearchValues, setLastSearchValues] = useState(null); const columnInfo = useMemo(() => { const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList; @@ -36,65 +62,193 @@ const PumpingStationRvw = () => { }; }, []); + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [statsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const submitDialogContext = useMemo(() => { + if (reviewOpinionContext == null) { + return null; + } + + const instSeCd = userInfo?.instSeCd ?? ""; + if (!instSeCd) { + return null; + } + + return { + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + instSeCd, + }; + }, [reviewOpinionContext, userInfo?.instSeCd]); + const handleReset = () => { setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + setLastSearchValues(null); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); }; const handleSearch = async (values: SearchFormValues) => { - if (!values.sido || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); return; } + setLastSearchValues(values); + try { setIsLoading(true); const response = await statisticsManagementApi.getMediationPumpStationList({ ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sggNm: values.gugun === "ALL" ? "" : values.gugun, sewerBzmnCd: values.bsnmCd, statsYr: values.year, pumpstNm: values.fctName, }); - if (response.status === 'success' && response.data) { - const rawList = response.data as MediationPumpStationItem[]; - - const convertedData = rawList.map(item => ({ + if (response.status === "success" && response.data) { + const rawList = extractMediationPumpStationList(response.data); + const statsInfo = extractMediationPumpStationStatsStatusInfo(response.data); + + const convertedData = rawList.map((item: MediationPumpStationItem) => ({ ...item, - rmrk: item.rmrk || '' + admdstNm: item.admdstNm ?? "", + pumpstNm: item.fcltNm ?? "", + impdFcltPstnCn: item.fcltPstnCn ?? "", + pumpSeNm: item.pumpSeNm ?? "", + swtrmtFcltNm: item.swtrmtFcltNm ?? "", + oprtngStrtYmd: item.oprtngStrtYmd ?? "", + exlnPumpN4RsrvCnt: item.exlnPumpN1RsrvCnt ?? 0, + rmrk: item.rmrk ?? "", })); + setStatsStatusInfo(statsInfo); + setReviewOpinionContext({ + statsYr: values.year ?? "", + sewerBzmnCd: values.bsnmCd ?? "", + statsCd: statsInfo.statsCd ?? "", + }); + setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? ""); + setLatestProcessHistory(statsInfo.wrterOpnnCn ?? ""); setRowData(toGridRows(convertedData)); } else { setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); alert(response.message || "데이터가 없습니다."); } - } catch (error) { - console.error("중계펌프장 조회 중 에러 발생:", error); - alert('데이터 조회 중 오류가 발생했습니다.'); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; - const approveFunc = () => console.log('approveFunc'); - const saveFunc = () => console.log('submit'); - const reviewOpinionFunc = () => console.log('reviewOpinionFunc'); - const returnFunc = () => console.log('returnFunc'); + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setLatestReviewOpinion(row?.splmntOpnnCn ?? ""); + setLatestProcessHistory(row?.wrterOpnnCn ?? ""); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setStatsStatusInfo((prev) => ({ + ...prev, + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + })); + setLatestReviewOpinion(param.splmntOpnnCn); + setLatestProcessHistory(param.wrterOpnnCn); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; return (
-
-
- - - - + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && } +
-
+ )} {isLoading && (
@@ -104,16 +258,48 @@ const PumpingStationRvw = () => {
)} - + +
+ + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default PumpingStationRvw; \ No newline at end of file +export default PumpingStationRvw; diff --git a/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx b/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx index c491e1552..84f6aad10 100644 --- a/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx +++ b/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx @@ -65,7 +65,6 @@ export function ReviewSubmitDialog({ if (cancelled) return; if (response.status !== "success" || !response.data) { - alert("제출 정보를 불러오지 못했습니다."); setReviewerInfo(null); return; } @@ -73,7 +72,6 @@ export function ReviewSubmitDialog({ setReviewerInfo(response.data); } catch { if (!cancelled) { - alert("제출 정보를 불러오지 못했습니다."); setReviewerInfo(null); } } finally { @@ -91,7 +89,7 @@ export function ReviewSubmitDialog({ }, [open, context, alert]); const handleConfirm = async () => { - if (context == null || reviewerInfo == null) { + if (context == null) { alert("제출 정보를 확인한 뒤 다시 시도해주세요."); return; } @@ -105,8 +103,8 @@ export function ReviewSubmitDialog({ sewerBzmnCd: context.sewerBzmnCd, statsCd: context.statsCd, mdfrId: userInfo.userUnqId, - splmntOpnnCn: reviewerInfo.splmntOpnnCn ?? "", - wrterOpnnCn: reviewerInfo.wrterOpnnCn ?? "", + splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "", + wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "", }; const response = await statisticsApi.submitReview(submitParam); @@ -177,7 +175,7 @@ export function ReviewSubmitDialog({ type="button" color="primary" onClick={() => void handleConfirm()} - disabled={isLoading || isSubmitting || reviewerInfo == null} + disabled={isLoading || isSubmitting} > 제출 diff --git a/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx b/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx index bd594d334..2b52f7bed 100644 --- a/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx @@ -1,53 +1,434 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewageExcretaRelatedReviewList, + extractSewageExcretaRelatedReviewStatsStatusInfo, + mapSewageExcretaRelatedReviewRows, + type SewageExcretaRelatedReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewage-excreta-related-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업 +const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001"; + +async function fetchExcretaRelatedReviewTab( + tabKey: string, + searchParam: SewageExcretaRelatedReviewRequestParams, +) { + if (tabKey === "ST00120001") { + return statisticsManagementApi.getPublicSewageStaffReviewList(searchParam); + } + if (tabKey === "ST00120002") { + return statisticsManagementApi.getPublicHumanWasteStaffReviewList(searchParam); + } + if (tabKey === "ST00120003") { + return statisticsManagementApi.getHumanWasteCollectionReviewList(searchParam); + } + if (tabKey === "ST00120004") { + return statisticsManagementApi.getIndvSwtrmtRelBsnsReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업 const SewageExcretaRelatedRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewageExcretaRelatedInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewageExcretaRelatedReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchExcretaRelatedReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewageExcretaRelatedReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewageExcretaRelatedReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchExcretaRelatedReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewageExcretaRelatedReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewageExcretaRelatedReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - export default SewageExcretaRelatedRvw; diff --git a/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx b/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx index d0dbcb383..04caffe8e 100644 --- a/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx @@ -1,52 +1,435 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewageExcretaSlurgeReviewList, + extractSewageExcretaSlurgeReviewStatsStatusInfo, + mapSewageExcretaSlurgeReviewRows, + type SewageExcretaSlurgeReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 +const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001"; + +async function fetchSludgeReviewTab( + tabKey: string, + searchParam: SewageExcretaSlurgeReviewRequestParams, +) { + if (tabKey === "ST00110001") { + return statisticsManagementApi.getSludgeProcessingReviewList(searchParam); + } + if (tabKey === "ST00110002") { + return statisticsManagementApi.getSludgeDigestionTankReviewList(searchParam); + } + if (tabKey === "ST00110003") { + return statisticsManagementApi.getSludgeFacilityOperReviewList(searchParam); + } + if (tabKey === "ST00110004") { + return statisticsManagementApi.getHumanWasteSludgeReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 const SewageExcretaSlurgeRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewageExcretaSludgeInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewageExcretaSlurgeReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchSludgeReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewageExcretaSlurgeReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewageExcretaSlurgeReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchSludgeReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewageExcretaSlurgeReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewageExcretaSlurgeReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; -export default SewageExcretaSlurgeRvw; \ No newline at end of file +export default SewageExcretaSlurgeRvw; diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index 295802cdc..e01b33cb3 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -1,21 +1,33 @@ import { useMemo, useRef, useState } from "react"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useUserStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; -import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + SewerPipeMtcRequestParams, + StatsStatusInfo, +} from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; -import ApproveButton from "@/components/button/ApproveButton"; +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; -import SaveButton from "@/components/button/SaveButton"; import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn"; -import { toGridRows } from "@/components/util/grid/gridFunction"; - +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001"; @@ -50,17 +62,36 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] { return []; } +function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null { + if (!data || typeof data !== "object") return null; + const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo; + return ssi && typeof ssi === "object" ? ssi : null; +} + // 하수도 통계 검토 03. 하수관로 유지 관리 const SewagePipeMaintainRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); const searchParamRef = useRef(null); const searchDoneRef = useRef(false); const lazyTabLoggedRef = useRef>(new Set()); + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + const columnInfo = useMemo(() => { return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({ ...tab, @@ -72,9 +103,66 @@ const SewagePipeMaintainRvw = () => { })); }, []); + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + const handleReset = () => { setRowDataMap(new Map()); setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); searchParamRef.current = null; searchDoneRef.current = false; lazyTabLoggedRef.current.clear(); @@ -88,6 +176,7 @@ const SewagePipeMaintainRvw = () => { try { setIsLoading(true); + setLastSearchValues(values); const searchParam: SewerPipeMtcRequestParams = { statsYr: values.year, @@ -103,6 +192,12 @@ const SewagePipeMaintainRvw = () => { const response = await fetchSewagePipeMaintainReviewTab(activeTab, searchParam); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi ?? {}); + return next; + }); const listData = getListDataByTab(activeTab, response.data); setRowDataMap((prev) => { const next = new Map(prev); @@ -110,9 +205,11 @@ const SewagePipeMaintainRvw = () => { return next; }); } else { - alert(response?.message || "데이터가 없습니다."); + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); } } catch { + setTabStatsStatusInfoMap(new Map()); alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); @@ -143,6 +240,12 @@ const SewagePipeMaintainRvw = () => { lazyTabLoggedRef.current.add(tabKey); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi ?? {}); + return next; + }); setRowDataMap((prev) => { const next = new Map(prev); const listData = getListDataByTab(tabKey, response.data); @@ -159,18 +262,96 @@ const SewagePipeMaintainRvw = () => { } }; + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; + return (
-
-
- { }} /> - { }} /> - { }} /> - { }} /> + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
-
+ )} {isLoading && (
@@ -228,14 +409,39 @@ const SewagePipeMaintainRvw = () => { showHeaderCheckbox={false} fixedColumns={info.fixedColumns} enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} /> ); })}
+ + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default SewagePipeMaintainRvw; \ No newline at end of file +export default SewagePipeMaintainRvw; diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 8bd0f9e44..cfb70260d 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -32,7 +32,7 @@ import type { } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001"; @@ -459,6 +459,7 @@ const SewagePipeRvw = () => { showHeaderCheckbox={false} fixedColumns={info.fixedColumns} enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} /> ); diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index 4a250d2aa..3c9f2d34d 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -1,19 +1,72 @@ -import { useState, useMemo, useRef } from "react"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; -import ReturnButton from "@/components/button/ReturnButton"; -import { LoadingWithAnimation } from "@common/components"; +import { useMemo, useRef, useState } from "react"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useUserStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; -import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; -import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; -import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; +import ReturnButton from "@/components/button/ReturnButton"; +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; + +const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001"; + +async function fetchSewageReservoirPumpReviewTab( + tabKey: string, + searchParam: SewerReservoirRequestParams, +) { + if (tabKey === "ST00040001") { + return statisticsManagementApi.getSewerReservoirList(searchParam); + } + if (tabKey === "ST00040002") { + return statisticsManagementApi.getPumpStationList(searchParam); + } + return null; +} + +function getListDataByTab(tabKey: string, responseData: unknown): object[] { + if (Array.isArray(responseData)) { + return responseData as object[]; + } + if (!responseData || typeof responseData !== "object") { + return []; + } + + const data = responseData as Record; + if (tabKey === "ST00040001") { + const list = data.sewerReservoirList ?? data.facilitiesList; + return Array.isArray(list) ? (list as object[]) : []; + } + if (tabKey === "ST00040002") { + const list = data.pumpStationList ?? data.pumpingStationList; + return Array.isArray(list) ? (list as object[]) : []; + } + return []; +} + +function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null { + if (!data || typeof data !== "object" || Array.isArray(data)) return null; + const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo; + return ssi && typeof ssi === "object" ? ssi : null; +} /** * 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장 @@ -22,11 +75,26 @@ import type { SewerReservoirRequestParams } from "@/entities/statistics/model/se */ const SewageReservoirPumpRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState('ST00040001'); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); const columnInfo = useMemo(() => { return SewageReservoirPumpInfoListRvw.map((tab: ColumnInfoList) => ({ @@ -39,44 +107,116 @@ const SewageReservoirPumpRvw = () => { })); }, []); + const activeStatsCd = activeStatsStatusInfo.statsCd ?? activeTab; + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsCd, + }; + }, [activeStatsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsCd; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + const handleReset = () => { setRowDataMap(new Map()); - setActiveTab('ST00040001'); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; const handleSearch = async (values: SearchFormValues) => { - if (!values.sido || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); return; } - setActiveTab('ST00040001'); - setRowDataMap(new Map()); - try { setIsLoading(true); + setLastSearchValues(values); - const searchParam: SewerReservoirRequestParams = { + const searchParam: SewerReservoirRequestParams = { statsYr: values.year, sewerBzmnCd: values.bsnmCd, ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sggNm: values.gugun === "ALL" ? "" : values.gugun, }; searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); - const response = await statisticsManagementApi.getSewerReservoirList(searchParam); + const response = await fetchSewageReservoirPumpReviewTab(activeTab, searchParam); - if (response?.status === 'success' && response.data) { - const newRowDataMap = new Map[]>(); - newRowDataMap.set('ST00040001', toGridRows(response.data as object[])); - setRowDataMap(newRowDataMap); + if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi ?? {}); + return next; + }); + const listData = getListDataByTab(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(listData)); + return next; + }); } else { - alert(response?.message || "데이터가 없습니다."); + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); } } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } @@ -85,38 +225,119 @@ const SewageReservoirPumpRvw = () => { const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); - const existingAData = rowDataMap.get('ST00040001'); const activeTabData = rowDataMap.get(tabKey); - - const hasAData = Array.isArray(existingAData) && existingAData.length > 0; const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; - if (hasAData && !hasActiveTabData && searchParamRef.current) { - try { - setIsLoading(true); + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; - let response; + if (!canLazyLoad) return; - if (tabKey === 'ST00040001') { - response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current); - } else if (tabKey === 'ST00040002') { - response = await statisticsManagementApi.getPumpStationList(searchParamRef.current); - } + const searchParam = searchParamRef.current; + if (!searchParam) return; - if (response?.status === 'success' && response.data) { - setRowDataMap(prev => { - const next = new Map(prev); - next.set(tabKey, toGridRows(response.data as object[])); - return next; - }); - } else if (response) { - alert(response?.message || "데이터가 없습니다."); - } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); + try { + setIsLoading(true); + + const response = await fetchSewageReservoirPumpReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi ?? {}); + return next; + }); + const listData = getListDataByTab(tabKey, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, toGridRows(listData)); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); } }; @@ -124,14 +345,19 @@ const SewageReservoirPumpRvw = () => {
-
-
- { }} /> - { }} /> - { }} /> - { }} /> + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
-
+ )} {isLoading && (
@@ -141,16 +367,87 @@ const SewageReservoirPumpRvw = () => {
)} - + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} + +
+ + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default SewageReservoirPumpRvw; \ No newline at end of file +export default SewageReservoirPumpRvw; diff --git a/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx b/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx index 68a5b6cc0..1757350db 100644 --- a/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx +++ b/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx @@ -1,54 +1,432 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewerageFinanceReviewList, + extractSewerageFinanceReviewStatsStatusInfo, + mapSewerageFinanceReviewRows, + type SewerageFinanceReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewerage-finance-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; +const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001"; -// 하수도 통계 관리 09. 하수도 재정 +async function fetchSewerageFinanceReviewTab( + tabKey: string, + searchParam: SewerageFinanceReviewRequestParams, +) { + if (tabKey === "ST00090001") { + return statisticsManagementApi.getSewerageFinanceSettleReviewList(searchParam); + } + if (tabKey === "ST00090002") { + return statisticsManagementApi.getSewerageChargeReviewList(searchParam); + } + if (tabKey === "ST00090003") { + return statisticsManagementApi.getSewerageUsageFeeReviewList(searchParam); + } + if (tabKey === "ST00090004") { + return statisticsManagementApi.getCauserBurdenReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 09. 하수도 재정 const SewerageFinanceRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewerageFinanceInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewerageFinanceReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); + + const response = await fetchSewerageFinanceReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewerageFinanceReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewerageFinanceReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchSewerageFinanceReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewerageFinanceReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewerageFinanceReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - -export default SewerageFinanceRvw; \ No newline at end of file +export default SewerageFinanceRvw; diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index c13c93dd5..e55fa0cd8 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -18,7 +18,7 @@ import type { import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; import { ReviewOpinionDialog, type ReviewOpinionDialogContext, @@ -245,6 +245,7 @@ const SeweragePenetrationRateRvw = () => { rowStartNum={columnInfo.rowStartNum} rowData={rowData} setRowData={setRowData} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} /> { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewerageServiceLevelInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewerageServiceLevelReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchServiceLevelReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewerageServiceLevelReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewerageServiceLevelReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchServiceLevelReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewerageServiceLevelReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewerageServiceLevelReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - export default SewerageServiceLevelRvw; diff --git a/apps/st/src/components/statistics/review/WaterReuseRvw.tsx b/apps/st/src/components/statistics/review/WaterReuseRvw.tsx index e642a4448..ea2c16854 100644 --- a/apps/st/src/components/statistics/review/WaterReuseRvw.tsx +++ b/apps/st/src/components/statistics/review/WaterReuseRvw.tsx @@ -1,51 +1,436 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractWaterReuseReviewList, + extractWaterReuseReviewStatsStatusInfo, + mapWaterReuseReviewRows, + type WaterReuseReviewRequestParams, +} from "@/entities/statistics/model/statistics-water-reuse-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn"; +import { reviewRowTypeCurrCellStyleCallback, toGridRows } from "@/components/util/grid/gridFunction"; -import { WaterReuseInfoList } from "@/components/util/column/review/WaterReuseRvwColumn"; +const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001"; +async function fetchWaterReuseReviewTab( + tabKey: string, + searchParam: WaterReuseReviewRequestParams, +) { + if (tabKey === "ST00100001") { + return statisticsManagementApi.getWaterReuseReuseStatusReviewList(searchParam); + } + if (tabKey === "ST00100002") { + return statisticsManagementApi.getWaterReuseGrayWaterReviewList(searchParam); + } + if (tabKey === "ST00100003") { + return statisticsManagementApi.getWaterReuseRainwaterReviewList(searchParam); + } + if (tabKey === "ST00100004") { + return statisticsManagementApi.getWaterReuseRegStatusReviewList(searchParam); + } + if (tabKey === "ST00100005") { + return statisticsManagementApi.getWaterReuseSncnStatusReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 10. 물 재이용 const WaterReuseRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return WaterReuseInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: WaterReuseReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchWaterReuseReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractWaterReuseReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractWaterReuseReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapWaterReuseReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchWaterReuseReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractWaterReuseReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractWaterReuseReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapWaterReuseReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + if (!info.columns || !info.columnLayout) { + return null; + } + + const rowData = rowDataMap.get(tabKey) ?? []; + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const n = + typeof updater === "function" ? updater(cur) : updater; + next.set(tabKey, n); + return next; + }); + }} + isExcelImport={false} + isExcelExport + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + cellStyleCallback={reviewRowTypeCurrCellStyleCallback} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; -export default WaterReuseRvw; \ No newline at end of file +export default WaterReuseRvw; diff --git a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx index 4a3e1cc56..cbdf98747 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx @@ -3,7 +3,6 @@ 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"; diff --git a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx index c251aff5c..ffd2db631 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx @@ -118,7 +118,7 @@ const loadGridData = async (tabKey: string, values: SearchFormValues): Promise { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 3. 민원유형 (단위: 건) */ - { name: 'CVPL_CM', fieldName: '총계(민원)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: '악취', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: '하수관로', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: '배수설비', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: '맨홀', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: '정화조', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: '하수도요금', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_ETC', fieldName: '기타(민원)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 성과 지표 및 비고 */ - { name: 'STDR_POPLTN_PER_CVPL', fieldName: '기준인구당 민원건수(건/10,000명)', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - 'POPLTN_SM', - { - name: 'complaintTypeGroup', - header: { text: '민원유형(건)' }, - items: [ - 'CVPL_CM', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_BSML', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_ETC' - ] - }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '주민친화시설', - rowStartNum: 2, - fixedColumns: 5, - columns: [ - /* 1. 기본 및 시설 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' } }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 2. 처리장 전체 면적 */ - { name: 'PRCPLT_AR', fieldName: '처리장 면적(㎡)', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 주민친화시설 면적 상세 (단위: ㎡) */ - { name: 'TOTAL_AR', fieldName: '총계(시설)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: '휴게시설', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: '어린이놀이공간', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: '체력단련시설', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: '운동시설', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: '주차장', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: '교육 및 문화시설', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: '습지조성(생태공원)', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: '기타(시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 비고 */ - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'YR_GBN', - 'PRCPLT_AR', - { - name: 'friendlyFacilityGroup', - header: { text: '주민친화시설 면적(㎡)' }, - items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' - ] - }, - 'RMK' - ] + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; -export const ExcretaTreatmentFacilityInfoList = rawServiceLevelList.map(tab => ({ +// 하수도 통계 검토 07. 분뇨처리시설 +const rawExcretaTreatmentFacilityList = [ + { + tabKey: "ST00070001", + tabLabel: "분뇨처리시설", + rowStartNum: 3, + fixedColumns: 4, + columns: [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" } }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "소재지" } }, + { name: "telNo", fieldName: "telNo", header: { text: "전화번호" } }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "준공일" } }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일" } }, + { + name: "wholPjtcoAmt", + fieldName: "wholPjtcoAmt", + header: { text: "사업비\n(백만원)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "prcsFcltCpct", + fieldName: "prcsFcltCpct", + header: { text: "시설용량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "cpct", + fieldName: "cpct", + header: { text: "처리량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "oprtngDayCnt", + fieldName: "oprtngDayCnt", + header: { text: "가동일수\n(일/년)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { name: "prcsMthdExpln", fieldName: "prcsMthdExpln", header: { text: "처리공법" } }, + { name: "linkTmplntCn", fieldName: "linkTmplntCn", header: { text: "연계처리장명" } }, + { + name: "linkPrcsQnt", + fieldName: "linkPrcsQnt", + header: { text: "연계처리량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "inflwBodWqVl", + fieldName: "inflwBodWqVl", + header: { text: "BOD" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTocWqVl", + fieldName: "inflwTocWqVl", + header: { text: "TOC" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwSsWqVl", + fieldName: "inflwSsWqVl", + header: { text: "SS" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTnWqVl", + fieldName: "inflwTnWqVl", + header: { text: "T-N" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTpWqVl", + fieldName: "inflwTpWqVl", + header: { text: "T-P" }, + footer: footerDecimal3ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.000", + }, + { + name: "inflwClfgCnt", + fieldName: "inflwClfgCnt", + header: { text: "대장균\n군수" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "dschgBodWqVl", + fieldName: "dschgBodWqVl", + header: { text: "BOD" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTocWqVl", + fieldName: "dschgTocWqVl", + header: { text: "TOC" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgSsWqVl", + fieldName: "dschgSsWqVl", + header: { text: "SS" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTnWqVl", + fieldName: "dschgTnWqVl", + header: { text: "T-N" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTpWqVl", + fieldName: "dschgTpWqVl", + header: { text: "T-P" }, + footer: footerDecimal3ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.000", + }, + { + name: "dschgClfgCnt", + fieldName: "dschgClfgCnt", + header: { text: "대장균\n군수" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { 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: "operMthdCdCn", fieldName: "operMthdCdCn", header: { text: "운영주체\n(자체/공기업/\n민간위탁)" } }, + { + name: "wholNope", + fieldName: "wholNope", + header: { text: "직원총수\n(명)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "sftyAcdntEvntcsCnt", + fieldName: "sftyAcdntEvntcsCnt", + header: { text: "안전사고\n건수\n(건/인/년)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + ], + columnLayout: [ + { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + ], + }, + "displayYear", + "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", + ], + }, +]; + +export const ExcretaTreatmentFacilityInfoList = rawExcretaTreatmentFacilityList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts b/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts index c626ba504..dd6df776f 100644 --- a/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts +++ b/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts @@ -1,139 +1,336 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 08.개인하수처리시설 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerDecimal2ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + ], +}; + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 08. 개인하수처리시설 const rawPrivateSewageList = [ { - tabKey: 'a', - tabLabel: '처리용량별 오수처리시설 현황', + tabKey: "ST00080001", + tabLabel: "처리용량별 오수처리시설", rowStartNum: 3, fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'STTY_SM', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_SM', fieldName: '계(처리구역 내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_2', fieldName: '2이하(처리구역 내)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_3', fieldName: '3~10(처리구역 내)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_11', fieldName: '11~20(처리구역 내)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_21', fieldName: '21~50(처리구역 내)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_51', fieldName: '51~100(처리구역 내)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_101', fieldName: '101~500(처리구역 내)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_501', fieldName: '501이상(처리구역 내)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_SM', fieldName: '계(처리구역 외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_2', fieldName: '2이하(처리구역 외)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_3', fieldName: '3~10(처리구역 외)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_11', fieldName: '11~20(처리구역 외)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_21', fieldName: '21~50(처리구역 외)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_51', fieldName: '51~100(처리구역 외)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_101', fieldName: '101~500(처리구역 외)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_501', fieldName: '501이상(처리구역 외)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewAddressColumns, + intCol("wholDrtwPrcsFcltCnt", "총계"), + intCol("wholStaaCnt", "계"), + intCol("step1StaaPrcsQnt", "2이하"), + intCol("step2StaaPrcsQnt", "3~10"), + intCol("step3StaaPrcsQnt", "11~20"), + intCol("step4StaaPrcsQnt", "21~50"), + intCol("step5StaaPrcsQnt", "51~100"), + intCol("step6StaaPrcsQnt", "101~500"), + intCol("step7StaaPrcsQnt", "501이상"), + intCol("staaExcpWhol", "계"), + intCol("step1StaaExcpPrcsQnt", "2이하"), + intCol("step2StaaExcpPrcsQnt", "3~10"), + intCol("step3StaaExcpPrcsQnt", "11~20"), + intCol("step4StaaExcpPrcsQnt", "21~50"), + intCol("step5StaaExcpPrcsQnt", "51~100"), + intCol("step6StaaExcpPrcsQnt", "101~500"), + intCol("step7StaaExcpPrcsQnt", "501이상"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'STTY_SM', + reviewAddressLayout, + "displayYear", + "wholDrtwPrcsFcltCnt", { - name: 'individualGroup', header: { text: '오수처리시설' }, + name: "privateSewageFacilityGroup", + header: { text: "오수처리시설" }, items: [ { - name: 'innerGroup', header: { text: '하수처리구역 내(㎥/일)' }, - 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'] + name: "insideGroup", + header: { text: "하수처리구역 내(㎥/일)" }, + items: [ + "wholStaaCnt", + "step1StaaPrcsQnt", + "step2StaaPrcsQnt", + "step3StaaPrcsQnt", + "step4StaaPrcsQnt", + "step5StaaPrcsQnt", + "step6StaaPrcsQnt", + "step7StaaPrcsQnt", + ], }, { - name: 'outerGroup', header: { text: '하수처리구역 외(㎥/일)' }, - 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'] - } - ] + name: "outsideGroup", + header: { text: "하수처리구역 외(㎥/일)" }, + items: [ + "staaExcpWhol", + "step1StaaExcpPrcsQnt", + "step2StaaExcpPrcsQnt", + "step3StaaExcpPrcsQnt", + "step4StaaExcpPrcsQnt", + "step5StaaExcpPrcsQnt", + "step6StaaExcpPrcsQnt", + "step7StaaExcpPrcsQnt", + ], + }, + ], }, - 'RMK' - ] + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '정화조현황', + tabKey: "ST00080002", + tabLabel: "정화조현황", rowStartNum: 4, - fixedColumns: 1, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_FCLTY_CNT', fieldName: '총 시설 수(A+D)', header: { text: '총 시설 수\n(A+D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_SM', fieldName: '총계 A=(B+C)/2', header: { text: '총계\nA=(B+C)/2' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_NPFL', fieldName: '자연유하', header: { text: '자연유하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ENFCT_PUMP', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_TFM', fieldName: '살수여상', header: { text: '살수여상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ARTM', fieldName: '폭기방법', header: { text: '폭기방법' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_CTARM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ETC', fieldName: '기타(방식)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_SM', fieldName: '계(C)', header: { text: '계(C)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_1', fieldName: '10인조 이하', header: { text: '10인조 이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_8', fieldName: '1,000이상', header: { text: '1,000이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } + ...reviewAddressColumns, + intCol("grssFcltCnt", "총 시설 수\n(A+D)"), + intCol("staaInsdWhol", "총계\nA=(B+C)/2"), + intCol("staaInsdPrcsCpctByWhol", "계(C)"), + intCol("staaInsdSttkNprsflCnt", "자연유하"), + intCol("step1StaaInsdSttkFrcCnt", "강제펌핑식"), + intCol("staaInsdSprktpFltrtbCnt", "살수여상"), + intCol("staaInsdAertMthdCnt", "폭기방법"), + intCol("staaInsdCntcAertCnt", "접촉폭기"), + intCol("staaInsdEtcCnt", "기타"), + intCol("step1StaaInsdNope", "10인조\n이하"), + intCol("step2StaaInsdNope", "11~20"), + intCol("step3StaaInsdNope", "21~30"), + intCol("step4StaaInsdNope", "31~50"), + intCol("step5StaaInsdNope", "51~199"), + intCol("step6StaaInsdNope", "200~499"), + intCol("step7StaaInsdNope", "500~999"), + intCol("step8StaaInsdNope", "1,000이상"), + intCol("staaExcpWhol", "총계\nD=(E+F)/2"), + intCol("staaExcpPrcsCpctByWhol", "계(F)"), + intCol("staaExcpSttkNprsflCnt", "자연유하"), + intCol("step1StaaExcpSttkFrcCnt", "강제펌핑식"), + intCol("staaExcpSprktpFltrtbCnt", "살수여상"), + intCol("staaExcpAertMthdCnt", "폭기방법"), + intCol("staaExcpCntcAertCnt", "접촉폭기"), + intCol("staaExcpEtcCnt", "기타"), + intCol("step1StaaExcpNope", "10인조\n이하"), + intCol("step2StaaExcpNope", "11~20"), + intCol("step3StaaExcpNope", "21~30"), + intCol("step4StaaExcpNope", "31~50"), + intCol("step5StaaExcpNope", "51~199"), + intCol("step6StaaExcpNope", "200~499"), + intCol("step7StaaExcpNope", "500~999"), + intCol("step8StaaExcpNope", "1,000이상"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'TOT_FCLTY_CNT', + reviewAddressLayout, + "displayYear", + "grssFcltCnt", { - name: 'innerMainGroup', header: { text: '하수처리구역 내' }, + name: "insideSewerGroup", + header: { text: "하수처리구역 내" }, items: [ - 'SPZI_SM', + "staaInsdWhol", { - name: 'innerMethodGroup', header: { text: '처리방식별' }, - items: [{ name: 'innerTankGroup', header: { text: '부패탱크' }, items: ['SPZI_NPFL', 'SPZI_ENFCT_PUMP'] }, 'SPZI_TFM', 'SPZI_ARTM', 'SPZI_CTARM', 'SPZI_ETC'] + name: "insdBGroup", + header: { text: "계(B)" }, + items: [ + "staaInsdSttkNprsflCnt", + "step1StaaInsdSttkFrcCnt", + "staaInsdSprktpFltrtbCnt", + "staaInsdAertMthdCnt", + "staaInsdCntcAertCnt", + "staaInsdEtcCnt", + ], }, { - name: 'innerCpcGroup', 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'] - } - ] + name: "insdCGroup", + header: { text: "처리용량별" }, + items: [ + "staaInsdPrcsCpctByWhol", + "step1StaaInsdNope", + "step2StaaInsdNope", + "step3StaaInsdNope", + "step4StaaInsdNope", + "step5StaaInsdNope", + "step6StaaInsdNope", + "step7StaaInsdNope", + "step8StaaInsdNope", + ], + }, + ], }, - 'RMK' - ] + { + 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", + ], + }, + ], + }, + "rmrk", + ], }, { - tabKey: 'c', - tabLabel: '개인하수도 관리지역 지정공고 현황', + tabKey: "ST00080003", + tabLabel: "개인하수도 관리지역 지정공고 현황", rowStartNum: 3, - fixedColumns: 1, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'MGMT_AR', fieldName: '공고지역(개소)', header: { text: '공고지역\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'APPN_SCOPE', fieldName: '지정범위(㎢)', header: { text: '지정범위\n(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WRRTN_CNT', fieldName: '개소수(정화조)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WRRTN_CPC', fieldName: '처리용량(인/일)', header: { text: '처리용량\n(인/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'STTY_CNT', fieldName: '개소수(오수시설)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'STTY_CPC', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewAddressColumns, + intCol("indvSwagePbancRgnCnt", "공고지역\n(개소)"), + { + name: "indvSwageDsgnScpLen", + fieldName: "indvSwageDsgnScpLen", + header: { text: "지정범위\n(㎢)" }, + footer: footerDecimal2ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + intCol("spctnkGrssFcltCnt", "총시설수"), + { + name: "spctnkQnt", + fieldName: "spctnkQnt", + header: { text: "처리용량\n(인/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + intCol("drtwPrcsFcltGrssFcltCnt", "총시설수"), + { + name: "drtwPrcsFcltQnt", + fieldName: "drtwPrcsFcltQnt", + header: { text: "처리용량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', + reviewAddressLayout, + "displayYear", { - name: 'mgmtMainGroup', header: { text: '개인하수도관리지역공고현황\n(하수도법 제34조의2)' }, + name: "groupNotice", + header: { text: "개인하수도관리지역공고현황 (하수도법제34조의2)" }, items: [ - 'MGMT_AR', 'APPN_SCOPE', - { name: 'septictankGroup', header: { text: '정화조' }, items: ['WRRTN_CNT', 'WRRTN_CPC'] }, - { name: 'sewageFacGroup', header: { text: '오수시설' }, items: ['STTY_CNT', 'STTY_CPC'] } - ] + "indvSwagePbancRgnCnt", + "indvSwageDsgnScpLen", + { + name: "septicTankGroup", + header: { text: "정화조" }, + items: ["spctnkGrssFcltCnt", "spctnkQnt"], + }, + { + name: "sewageTreatmentGroup", + header: { text: "오수처리시설" }, + items: ["drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltQnt"], + }, + ], }, - 'RMK' - ] - } + "rmrk", + ], + }, ]; -export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map(tab => ({ +export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts b/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts index be414d00f..8f4cd757e 100644 --- a/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts @@ -1,135 +1,276 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업 -const rawExcretaRelatedList = [ - { - tabKey: 'a', - tabLabel: '공공기관 하수분야', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC', - { - name: 'techJobGroup', - header: { text: '기술직' }, - items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC'] - }, - 'ETC', 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '공공기관 분뇨분야', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC', - { - name: 'techJobGroup', - header: { text: '기술직' }, - items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC'] - }, - 'ETC', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '분뇨수집·운반업', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'ECSCS_SM', fieldName: '업체수(개소)', header: { text: '업체수(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ECSCS_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'VHCLE_SM', fieldName: '계(차량)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'THREE_BELOW', fieldName: '3톤이하', header: { text: '3톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'FOUR_BELOW', fieldName: '4.5톤이하', header: { text: '4.5톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'EIGHT_BELOW', fieldName: '8톤이하', header: { text: '8톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_VHCLE', fieldName: '기타(차량)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // name 중복 방지를 위해 ETC_VHCLE로 수정 권장 - { name: 'COLCT_TRSP_ACMSLT', fieldName: '수집·운반실적(㎥/년)', header: { text: '수집·운반실적\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'ECSCS_SM', 'ECSCS_BSMN', - { - name: 'vehicleGroup', - header: { text: '시설(차량) 확보현황(대수)' }, - items: ['VHCLE_SM', 'THREE_BELOW', 'FOUR_BELOW', 'EIGHT_BELOW', 'ETC_VHCLE'] - }, - 'COLCT_TRSP_ACMSLT', 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '개인하수처리관련업', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CNSTRCT_ENTRPS', fieldName: '업소수(시공업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CNSTRCT_BSMN', fieldName: '종사인원(명)_시공업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MNFCTUR_ENTRPS', fieldName: '업소수(제조업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MNFCTUR_BSMN', fieldName: '종사인원(명)_제조업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MGMT_ENTRPS', fieldName: '업소수(관리업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MGMT_BSMN', fieldName: '종사인원(명)_관리업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { name: 'constructGroup', header: { text: '개인하수처리시설 설계ㆍ시공업' }, items: ['CNSTRCT_ENTRPS', 'CNSTRCT_BSMN'] }, - { name: 'manufactureGroup', header: { text: '개인하수처리시설 제조업' }, items: ['MNFCTUR_ENTRPS', 'MNFCTUR_BSMN'] }, - { name: 'managementGroup', header: { text: '개인하수처리시설 관리업' }, items: ['MGMT_ENTRPS', 'MGMT_BSMN'] }, - 'RMK' - ] - } +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map(tab => ({ - ...tab, - columns: tab.columns.map(col => ({ - ...col, - editable: false - })) -})); \ No newline at end of file +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + // { + // name: "admdstNm", + // fieldName: "admdstNm", + // header: { text: "행정구역명" }, + // mergeCells: true, + // sortdisable: true, + // }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + // "admdstNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업 +const rawExcretaRelatedList = [ + { + tabKey: "ST00120001", + tabLabel: "공공기관 하수분야", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + textCol("ognzNm", "직제"), + intCol("empGrssCnt", "직원총수"), + intCol("pbadmsNope", "행정직"), + intCol("techWholNope", "계"), + intCol("engrkCnt", "토목"), + intCol("mchnCnt", "기계"), + intCol("prvtmCnt", "전기"), + intCol("chmengCnt", "화공"), + intCol("envCnt", "환경"), + intCol("enjbEtcCnt", "기타"), + intCol("etcCnt", "기타"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techJobGroup", + header: { text: "기술직" }, + items: [ + "techWholNope", + "engrkCnt", + "mchnCnt", + "prvtmCnt", + "chmengCnt", + "envCnt", + "enjbEtcCnt", + ], + }, + "etcCnt", + "rmrk", + ], + }, + { + tabKey: "ST00120002", + tabLabel: "공공기관 분뇨분야", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + textCol("ognzNm", "직제"), + intCol("empGrssCnt", "직원총수"), + intCol("pbadmsNope", "행정직"), + intCol("techWholNope", "계"), + intCol("engrkCnt", "토목"), + intCol("mchnCnt", "기계"), + intCol("prvtmCnt", "전기"), + intCol("chmengCnt", "화공"), + intCol("envCnt", "환경"), + intCol("enjbEtcCnt", "기타"), + intCol("etcCnt", "기타"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techJobGroup", + header: { text: "기술직" }, + items: [ + "techWholNope", + "engrkCnt", + "mchnCnt", + "prvtmCnt", + "chmengCnt", + "envCnt", + "enjbEtcCnt", + ], + }, + "etcCnt", + "rmrk", + ], + }, + { + tabKey: "ST00120003", + tabLabel: "분뇨수집·운반업", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("bzentyCnt", "업체수(개소)"), + intCol("empNope", "종사인원(명)"), + intCol("fcltSecrPrstCnt", "계"), + intCol("n3TonUdrCnt", "3톤이하"), + intCol("n4TonUdrCnt", "4.5톤이하"), + intCol("n8TonUdrCnt", "8톤이하"), + intCol("etcCnt", "기타"), + decimal1Col("clctCnvncPrfmncQnt", "수집·운반실적\n(㎥/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "bzentyCnt", + "empNope", + { + name: "vehicleGroup", + header: { text: "시설(차량) 확보현황(대수)" }, + items: [ + "fcltSecrPrstCnt", + "n3TonUdrCnt", + "n4TonUdrCnt", + "n8TonUdrCnt", + "etcCnt", + ], + }, + "clctCnvncPrfmncQnt", + "rmrk", + ], + }, + { + tabKey: "ST00120004", + tabLabel: "개인하수처리관련업", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("designMdsrCnt", "업소수"), + intCol("designMdsrNope", "종사인원(명)"), + intCol("mfbizCnt", "업소수"), + intCol("mfbizMdsrNope", "종사인원(명)"), + intCol("mngCnt", "업소수"), + intCol("mngMdsrNope", "종사인원(명)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { + name: "constructGroup", + header: { text: "개인하수처리시설 설계ㆍ시공업" }, + items: ["designMdsrCnt", "designMdsrNope"], + }, + { + name: "manufactureGroup", + header: { text: "개인하수처리시설 제조업" }, + items: ["mfbizCnt", "mfbizMdsrNope"], + }, + { + name: "managementGroup", + header: { text: "개인하수처리시설 관리업" }, + items: ["mngCnt", "mngMdsrNope"], + }, + "rmrk", + ], + }, +]; + +export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map((tab) => ({ + ...tab, + columns: tab.columns.map((col) => ({ + ...col, + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewageExcretaRelatedInfoListRvw` 사용 */ +export const SewageExcretaRelatedInfoList = SewageExcretaRelatedInfoListRvw; diff --git a/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts b/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts index b9dfcbf98..257c902a0 100644 --- a/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts @@ -1,199 +1,385 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewFacilityAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewFacilityAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + "sggNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 const rawSludgeList = [ { - tabKey: 'a', - tabLabel: '찌꺼기 처리시설 현황', + tabKey: "ST00110001", + tabLabel: "찌꺼기(슬러지)처리시설현황", rowStartNum: 4, fixedColumns: 2, columns: [ - /* 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 찌꺼기발생량 */ - { name: 'TOT_AMNT', fieldName: '합계(발생)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'P_AMNT', fieldName: '자체(발생)', header: { text: '자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_AMNT', fieldName: '외부유입량(발생)', header: { text: '외부유입량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 처리 정보 */ - { name: 'OUT_FCT_NM', fieldName: '이송처리장', header: { text: '이송처리장' } }, - { name: 'OUT_AMNT', fieldName: '이송처리량(㎥/년)', header: { text: '이송처리량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSP_MOIRAT', fieldName: '함수율(%,탈수기준)', header: { text: '함수율\n(%,\n탈수기준)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSP_SM', fieldName: '찌꺼기 처리량 합계(톤/년)', header: { text: '찌꺼기\n처리량\n합계(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 자체 찌꺼기 처리량 */ - { name: 'I_1CH_SM', fieldName: '자체찌꺼기 처리량소계', header: { text: '자체찌꺼기\n처리량소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_REC', fieldName: '매립(자체)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC', fieldName: '소각(자체)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_SM', fieldName: '소계(소각후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_REC', fieldName: '매립(소각후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_RAW', fieldName: '제품원료(소각후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_COV', fieldName: '복토재(소각후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_FCT', fieldName: '시설(업체명)_소각후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_SOL', fieldName: '고화(자체)', header: { text: '고화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_SM', fieldName: '소계(고화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_REC', fieldName: '매립(고화후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_COV', fieldName: '복토재(고화후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_FCT', fieldName: '시설(업체명)_고화후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_DRY', fieldName: '건조(자체)', header: { text: '건조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_SM', fieldName: '소계(건조후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_REC', fieldName: '매립(건조후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_INC', fieldName: '소각(건조후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_FUE', fieldName: '연료(건조후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_RAW', fieldName: '제품원료(건조후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_COV', fieldName: '복토재(건조후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_COM', fieldName: '퇴비화(건조후)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_FCT', fieldName: '시설(업체명)_건조후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_CAR', fieldName: '탄화(자체)', header: { text: '탄화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_SM', fieldName: '소계(탄화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_INC', fieldName: '소각(탄화후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_FUE', fieldName: '연료(탄화후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_FCT', fieldName: '시설(업체명)_탄화후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_COM', fieldName: '퇴비화(자체)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_COM_FCT', fieldName: '시설(업체명)_퇴비화', header: { text: '시설(업체명)' } }, - - /* 외부위탁 처리량 */ - { name: 'O_CON_SM', fieldName: '소계(외부위탁)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_REC', fieldName: '매립(외부위탁)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_INC', fieldName: '소각(외부위탁)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_FUE1', fieldName: '연료(외부위탁)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_RAW', fieldName: '제품원료(외부위탁)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_COV', fieldName: '복토재(외부위탁)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_COM', fieldName: '퇴비화(외부위탁)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_FCT_NM', fieldName: '시설(업체명)_외부위탁', header: { text: '시설(업체명)' } }, - - /* 기타 */ - { name: 'OM_1CH_SM', fieldName: '수도권광역 위탁처리량(톤/년)', header: { text: '수도권광역\n위탁처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "하수\n처리장명"), + decimal1Col("gnamWhol", "합계"), + decimal1Col("itslfQnt", "자체"), + decimal1Col("otsdInflwQnt", "외부유입량"), + textCol("transfTmplntNm", "이송처리장"), + decimal1Col("transfPrcsQnt", "이송처리량\n(㎥/년)"), + decimal1Col("srpFncRt", "함수율\n(%, 탈수기준)"), + decimal1Col("wholSrpPrcsQnt", "찌꺼기\n처리량 합계\n(톤/년)"), + decimal1Col("itslfTs1Whol", "자체찌꺼기\n처리량 소계"), + decimal1Col("itslfTs1RclmQnt", "매립"), + decimal1Col("itslfTs1IncnrQnt", "소각"), + decimal1Col("incnrPrcsWhol", "소계"), + decimal1Col("incnrPrcsLfvlm", "매립"), + decimal1Col("incnrPrcsPrdctRmtrlQnt", "제품원료"), + decimal1Col("incnrPrcsFmlyrQnt", "복토재"), + textCol("incnrPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1AltdQnt", "고화"), + decimal1Col("stbznPrcsWhol", "소계"), + decimal1Col("stbznPrcsLfvlm", "매립"), + decimal1Col("stbznPrcsFmlyrQnt", "복토재"), + textCol("stbznPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1DryQnt", "건조"), + decimal1Col("dryPrcsWhol", "소계"), + decimal1Col("dryPrcsLfvlm", "매립"), + decimal1Col("dryPrcsIncnrQnt", "소각"), + decimal1Col("dryPrcsFuelQnt", "연료"), + decimal1Col("dryPrcsPrdctRmtrlQnt", "제품원료"), + decimal1Col("dryPrcsFmlyrQnt", "복토재"), + decimal1Col("dryPrcsCmpstgQnt", "퇴비화"), + textCol("dryPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1FuelQnt", "탄화"), + decimal1Col("fuelPrcsWhol", "소계"), + decimal1Col("fuelPrcsIncnrQnt", "소각"), + decimal1Col("fuelPrcsFuelQnt", "연료"), + textCol("fuelPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1CmpstgQnt", "퇴비화"), + textCol("cmpsPrcsFcltBzentyNm", "시설(업체명)"), + decimal1Col("otsdCnsgnWhol", "소계"), + decimal1Col("otsdCnsgnRclmQnt", "매립"), + decimal1Col("otsdCnsgnIncnrQnt", "소각"), + decimal1Col("otsdCnsgnFuelPwplQnt", "연료"), + decimal1Col("otsdCnsgnPrdctRmtrlQnt", "제품원료"), + decimal1Col("otsdCnsgnFmlyrQnt", "복토재"), + decimal1Col("otsdCnsgnCmpstgQnt", "퇴비화"), + textCol("otsdCnsgnFcltNm", "시설(업체명)"), + decimal1Col("cparTs2Whol", "수도권광역\n위탁처리량\n(톤/년)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', 'YR_GBN', - { name: 'genAmntGroup', header: { text: '찌꺼기발생량(톤/년,탈수기준)' }, items: ['TOT_AMNT', 'P_AMNT', 'O_AMNT'] }, - 'OUT_FCT_NM', 'OUT_AMNT', 'DSP_MOIRAT', 'DSP_SM', + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", { - name: 'internalProcessGroup', header: { text: '자체 찌꺼기 처리량(톤/년)' }, + name: "gnamGroup", + header: { text: "찌꺼기발생량(톤/년, 탈수기준)" }, + items: ["gnamWhol", "itslfQnt", "otsdInflwQnt"], + }, + "transfTmplntNm", + "transfPrcsQnt", + "srpFncRt", + "wholSrpPrcsQnt", + { + name: "itslfProcessingGroup", + header: { text: "자체 찌꺼기 처리량 현황(톤/년)" }, items: [ - 'I_1CH_SM', 'I_1CH_REC', 'I_1CH_INC', - { name: 'inc2ndGroup', header: { text: '소각 후 처리(2차)' }, items: ['I_1CH_INC_2CH_SM', 'I_1CH_INC_2CH_REC', 'I_1CH_INC_2CH_RAW', 'I_1CH_INC_2CH_COV', 'I_1CH_INC_2CH_FCT'] }, - 'I_1CH_SOL', - { name: 'sol2ndGroup', header: { text: '고화 후 처리(2차)' }, items: ['I_1CH_SOL_2CH_SM', 'I_1CH_SOL_2CH_REC', 'I_1CH_SOL_2CH_COV', 'I_1CH_SOL_2CH_FCT'] }, - 'I_1CH_DRY', - { name: 'dry2ndGroup', header: { text: '건조 후 처리(2차)' }, items: ['I_1CH_DRY_2CH_SM', 'I_1CH_DRY_2CH_REC', 'I_1CH_DRY_2CH_INC', 'I_1CH_DRY_2CH_FUE', 'I_1CH_DRY_2CH_RAW', 'I_1CH_DRY_2CH_COV', 'I_1CH_DRY_2CH_COM', 'I_1CH_DRY_2CH_FCT'] }, - 'I_1CH_CAR', - { name: 'car2ndGroup', header: { text: '탄화 후 처리(2차)' }, items: ['I_1CH_CAR_2CH_SM', 'I_1CH_CAR_2CH_INC', 'I_1CH_CAR_2CH_FUE', 'I_1CH_CAR_2CH_FCT'] }, - { name: 'compostGroup', header: { text: '퇴비화' }, items: ['I_1CH_COM_FCT'] } - ] + "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: 'externalProcessGroup', header: { text: '외부(민간)위탁 처리량(톤/년)' }, - items: ['O_CON_SM', 'O_CON_REC', 'O_CON_INC', 'O_CON_FUE1', 'O_CON_RAW', 'O_CON_COV', 'O_CON_COM', 'O_CON_FCT_NM'] + name: "otsdGroup", + header: { text: "외부(민간) 위탁 처리량(톤/년)" }, + items: [ + "otsdCnsgnWhol", + "otsdCnsgnRclmQnt", + "otsdCnsgnIncnrQnt", + "otsdCnsgnFuelPwplQnt", + "otsdCnsgnPrdctRmtrlQnt", + "otsdCnsgnFmlyrQnt", + "otsdCnsgnCmpstgQnt", + "otsdCnsgnFcltNm", + ], }, - 'OM_1CH_SM', 'RMK' - ] + "cparTs2Whol", + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '소화조 운영현황', + tabKey: "ST00110002", + tabLabel: "소화조 운영현황", rowStartNum: 3, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'DGSTR_FNNC', fieldName: '소화조 유/무(Y/N)선택', header: { text: '소화조 유/무\n(Y/N)선택' } }, - { name: 'DGSTR_CPC', fieldName: '설계용량(㎥)', header: { text: '설계\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_OPR_CPC', fieldName: '실제운전용량(㎥)', header: { text: '실제운전\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_IN', fieldName: '투입량(㎥/일)', header: { text: '투입량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_TS', fieldName: '투입 TS 농도(%)', header: { text: '투입 TS 농도\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_YRSTP_RN_DYN', fieldName: '가동일수(연간)', header: { text: '가동일수\n(연간)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_OCCUR_GAS', fieldName: '발생가스량(㎥/일)', header: { text: '발생가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_SM', fieldName: '계(이용현황)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_SLE', fieldName: '판매', header: { text: '판매' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ELTGNR', fieldName: '발전', header: { text: '발전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ONSLF', fieldName: '자체활용', header: { text: '자체활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ETC', fieldName: '기타(CNG등)', header: { text: '기타(CNG등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_GAS', fieldName: '잉여가스량(㎥/일)', header: { text: '잉여가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "하수\n처리장명"), + textCol("digesterYn", "소화조 유/무\n(Y/N)선택"), + intCol("dsgnCpct", "설계\n용량(㎥)"), + intCol("actlOperCpct", "실제운전\n용량(㎥)"), + decimal1Col("inputQnt", "투입량\n(㎥/일)"), + decimal1Col("tsConc", "투입 TS 농도\n(%)"), + intCol("oprtngDays", "가동일수\n(연간)"), + decimal1Col("gnrtGasQnt", "발생가스량\n(㎥/일)"), + decimal1Col("gasTotal", "계"), + decimal1Col("gasSale", "판매"), + decimal1Col("gasGen", "발전"), + decimal1Col("gasSelf", "자체활용"), + decimal1Col("gasEtc", "기타(CNG등)"), + decimal1Col("surplusGas", "잉여가스량\n(㎥/일)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', 'YR_GBN', + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", { - name: 'digesterMainGroup', header: { text: '소화조 운영현황' }, + name: "operGroup", + header: { text: "소화조 운영현황" }, items: [ - 'DGSTR_FNNC', 'DGSTR_CPC', 'DGSTR_OPR_CPC', 'DGSTR_IN', 'DGSTR_TS', 'DGSTR_YRSTP_RN_DYN', 'DGSTR_OCCUR_GAS', - { name: 'gasUsageGroup', header: { text: '소화가스 이용현황(㎥/일)' }, items: ['DGSTR_SM', 'DGSTR_SLE', 'DGSTR_ELTGNR', 'DGSTR_ONSLF', 'DGSTR_ETC'] }, - 'DGSTR_GAS' - ] + "digesterYn", + "dsgnCpct", + "actlOperCpct", + "inputQnt", + "tsConc", + "oprtngDays", + "gnrtGasQnt", + { + name: "gasUsageGroup", + header: { text: "소화가스 이용현황(㎥/일)" }, + items: ["gasTotal", "gasSale", "gasGen", "gasSelf", "gasEtc"], + }, + "surplusGas", + ], }, - 'RMK' - ] + "rmrk", + ], }, { - tabKey: 'c', - tabLabel: '하수찌꺼기 처리시설 운영현황', + tabKey: "ST00110003", + tabLabel: "하수찌꺼기 처리시설 운영현황", rowStartNum: 2, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'SLG_FCT_NM', fieldName: '찌꺼기처리시설명', header: { text: '찌꺼기처리시설명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TRT_METHOD', fieldName: '처리방식', header: { text: '처리방식' } }, - { name: 'SLG_FCT_CPC', fieldName: '시설용량(톤/일)', header: { text: '시설용량\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OPR_CPC', fieldName: '연간처리량(톤/년)', header: { text: '연간처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ODY_RN_TIME', fieldName: '일평균 가동시간(1~24)', header: { text: '일평균\n가동시간\n(1~24)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YRSTP_RN_DYN', fieldName: '연중 가동일(1~330)', header: { text: '연중 가동일\n(1~330)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'OPR_RT', fieldName: '가동율', header: { text: '가동율' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "찌꺼기처리\n시설명"), + textCol("prcsMthExpln", "처리방식"), + decimal1Col("fcltCpct", "시설용량\n(톤/일)"), + decimal1Col("anlPrcsQnt", "연간처리량\n(톤/년)"), + decimal1Col("dayAvgOprtngHr", "일평균\n가동시간\n(1~24)"), + intCol("anlOprtngDayCnt", "연중 가동일\n(1~330)"), + decimal1Col("oprtngRt", "가동율(%)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SLG_FCT_NM', 'YR_GBN', - { name: 'sludgeFctOprGroup', header: { text: '처리시설 운영현황' }, items: ['TRT_METHOD', 'SLG_FCT_CPC', 'OPR_CPC', 'ODY_RN_TIME', 'YRSTP_RN_DYN', 'OPR_RT'] }, - 'RMK' - ] + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + { + name: "operStatusGroup", + header: { text: "처리시설 운영현황" }, + items: [ + "prcsMthExpln", + "fcltCpct", + "anlPrcsQnt", + "dayAvgOprtngHr", + "anlOprtngDayCnt", + "oprtngRt", + ], + }, + "rmrk", + ], }, { - tabKey: 'd', - tabLabel: '분뇨찌꺼기 처리현황', + tabKey: "ST00110004", + tabLabel: "분뇨찌꺼기(슬러지)처리현황", rowStartNum: 2, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'ECSCS_NM', fieldName: '분뇨 처리장명', header: { text: '분 뇨\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'OCTY', fieldName: '발생량(A)=(B)+(C)(㎥/년)', header: { text: '발생량\n(A)=(B)+(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PROCESS_CPCTY_SM', fieldName: '계(B)', header: { text: '계(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LAND_RCLM', fieldName: '육상매립', header: { text: '육상매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INCNR', fieldName: '소각', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'FUEL', fieldName: '연료화', header: { text: '연료화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CYFD_CPCTY', fieldName: '미처분량(이월량)(C)(㎥/년)', header: { text: '미처분량\n(이월량)\n(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OUT_BSC_FCT_NM', fieldName: '타시설 연계여부', header: { text: '타시설\n연계여부' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "분뇨\n처리장명"), + decimal1Col("gnam", "발생량\n(A)=(B)+(C)"), + decimal1Col("wholDspsQnt", "계(B)"), + decimal1Col("rcycQnt", "재활용"), + decimal1Col("olandLfvlm", "육상매립"), + decimal1Col("incnrQnt", "소각"), + decimal1Col("oceanInputQnt", "해양투기"), + decimal1Col("fuelQnt", "연료화"), + decimal1Col("etcQnt", "기타"), + decimal1Col("nondspsQnt", "미처분량\n(이월량)(C)"), + textCol("swtrmtFcltNm", "타시설\n연계여부"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'ECSCS_NM', 'YR_GBN', 'OCTY', - { name: 'disposalAmntGroup', header: { text: '처 분 량(㎥/년)' }, items: ['PROCESS_CPCTY_SM', 'RUSE', 'LAND_RCLM', 'INCNR', 'OCEAN_DPSL', 'FUEL', 'ETC'] }, - 'CYFD_CPCTY', 'OUT_BSC_FCT_NM', 'RMK' - ] - } + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "gnam", + { + name: "dspsGroup", + header: { text: "처분량" }, + items: [ + "wholDspsQnt", + "rcycQnt", + "olandLfvlm", + "incnrQnt", + "oceanInputQnt", + "fuelQnt", + "etcQnt", + ], + }, + "nondspsQnt", + "swtrmtFcltNm", + "rmrk", + ], + }, ]; -export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map(tab => ({ +export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewageExcretaSludgeInfoListRvw` 사용 */ +export const SewageExcretaSludgeInfoList = SewageExcretaSludgeInfoListRvw; diff --git a/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts b/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts index b3dc2c73e..f5a9665ef 100644 --- a/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts @@ -1,215 +1,369 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 09. 하수도 재정 -const rawFinanceList = [ - { - tabKey: 'a', - tabLabel: '하수도재정 결산', - rowStartNum: 3, - fixedColumns: 1, - columns: [ - /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; - /* 2. 재정별 사업비 (수입) */ - { name: 'FNANC_WCT_SM', fieldName: '계(수입)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'NTNXPN', fieldName: '중앙정부 지원액', header: { text: '중앙정부\n지원액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LCLEXPNS', fieldName: '특․광역시, 도', header: { text: '특․광역시, 도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LCLTY_CT', fieldName: '시,군,구 지자체', header: { text: '시,군,구 지자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWER_RNTFEE', fieldName: '하수도 사용료', header: { text: '하수도\n사용료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'AMNTBRNCSR', fieldName: '원인자 부담금', header: { text: '원인자\n부담금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_INCME', fieldName: '기타 잡수입', header: { text: '기타\n잡수입' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CYFD_AMOUNT', fieldName: '이월액', header: { text: '이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 부채액 */ - { name: 'DEBT_AMOUNT', fieldName: '부채액(백만원)', header: { text: '부채액\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 4. 용도별 사업비 (지출) */ - { name: 'PRPOS_WCT_SM', fieldName: '계(지출)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_SM', fieldName: '소계(처리장)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_FCLTY', fieldName: '시설비(처리장)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_RPAIR', fieldName: '개보수비(처리장)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_ORPNS', fieldName: '운영 유지비(처리장)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_ETC', fieldName: '기타(처리장)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_SM', fieldName: '소계(관로)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_FCLTY', fieldName: '시설비(관로)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_RPAIR', fieldName: '개보수비(관로)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_ORPNS', fieldName: '운영 유지비(관로)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_ETC', fieldName: '기타(관로)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_SM', fieldName: '소계(저류시설)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_FCLTY', fieldName: '시설비(저류시설)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_RPAIR', fieldName: '개보수비(저류시설)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ORPNS', fieldName: '운영 유지비(저류시설)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ETC', fieldName: '기타(저류시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_SM', fieldName: '소계(기타지출)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ACDNT_CYFD_AMOUNT', fieldName: '사고 이월액', header: { text: '사고\n이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'UNRCIV_AMOUNT', fieldName: '미수납액', header: { text: '미수납액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_AMOUNT', fieldName: '기타(지출)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 5. 비고 */ - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'ADST_CD', - { - name: 'financeGroup', - header: { text: '재정별 사업비(백만원)' }, - items: [ - 'FNANC_WCT_SM', 'NTNXPN', - { name: 'localGroup', header: { text: '지방비' }, items: ['LCLEXPNS', 'LCLTY_CT'] }, - 'SEWER_RNTFEE', 'AMNTBRNCSR', 'ETC_INCME', 'CYFD_AMOUNT' - ] - }, - 'DEBT_AMOUNT', - { - name: 'usageGroup', - header: { text: '용도별사업비(백만원)' }, - items: [ - 'PRPOS_WCT_SM', - { name: 'plantGroup', header: { text: '처리장' }, items: ['PRCPLT_SM', 'PRCPLT_FCLTY', 'PRCPLT_RPAIR', 'PRCPLT_ORPNS', 'PRCPLT_ETC'] }, - { name: 'pipeGroup', header: { text: '하수관로' }, items: ['SEWAGE_SM', 'SEWAGE_FCLTY', 'SEWAGE_RPAIR', 'SEWAGE_ORPNS', 'SEWAGE_ETC'] }, - { name: 'storageGroup', header: { text: '하수저류시설' }, items: ['SWGE_STRGE_SM', 'SWGE_STRGE_FCLTY', 'SWGE_STRGE_RPAIR', 'SWGE_STRGE_ORPNS', 'SWGE_STRGE_ETC'] }, - { name: 'etcUsageGroup', header: { text: '기타' }, items: ['ETC_SM', 'ACDNT_CYFD_AMOUNT', 'UNRCIV_AMOUNT', 'ETC_AMOUNT'] } - ] - }, - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '하수도 요금', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'FYER_LEVY_QY', fieldName: '연간조정량(㎥)(A)', header: { text: '연간조정량\n(㎥)\n(A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LEVY_AMOUNT', fieldName: '하수도사용료수익(원)(B)', header: { text: '하수도사용료수익\n(원)\n(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'AVRG_UNTPC', fieldName: '㎥당요금(원/㎥) C=(B/A)', header: { text: '㎥당요금(원/㎥)\nC=(B/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SMRIZE_PRMPC', fieldName: '총괄원가(원)(D)', header: { text: '총괄원가(원)\n(D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SMRIZE_UNIT_PRMPC', fieldName: '㎥당원가(원/㎥) E=(D/A)', header: { text: '㎥당원가(원/㎥)\nE=(D/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RLTY_RT', fieldName: '요금현실화율(%) F=(C/E*100)', header: { text: '요금현실화율(%)\nF=(C/E*100)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'FYER_LEVY_QY', 'LEVY_AMOUNT', 'AVRG_UNTPC', 'SMRIZE_PRMPC', 'SMRIZE_UNIT_PRMPC', 'RLTY_RT', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '하수도 사용료', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'LEVY_AMOUNT', fieldName: '부과액(총계)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LEVY_QY', fieldName: '부과량(총계)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_LEVY_AMOUNT', fieldName: '부과액(가정용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_LEVY_QY', fieldName: '부과량(가정용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_UNTPC', fieldName: '단가(가정용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_LEVY_AMOUNT', fieldName: '부과액(업무용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_LEVY_QY', fieldName: '부과량(업무용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_UNTPC', fieldName: '단가(업무용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_LEVY_AMOUNT', fieldName: '부과액(영업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_LEVY_QY', fieldName: '부과량(영업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_UNTPC', fieldName: '단가(영업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_AMOUNT', fieldName: '부과액(욕탕1)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_QY', fieldName: '부과량(욕탕1)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_UNTPC', fieldName: '단가(욕탕1)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_LEVY_AMOUNT', fieldName: '부과액(욕탕2)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_LEVY_QY', fieldName: '부과량(욕탕2)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_UNTPC', fieldName: '단가(욕탕2)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_AMOUNT', fieldName: '부과액(산업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_QY', fieldName: '부과량(산업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_UNTPC', fieldName: '단가(산업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_LEVY_AMOUNT', fieldName: '부과액(기타사용료)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_LEVY_QY', fieldName: '부과량(기타사용료)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_UNTPC', fieldName: '단가(기타사용료)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - { - name: 'totalGroup', header: { text: '총계' }, - items: [{ name: 'tAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['LEVY_AMOUNT'] }, { name: 'tQyUnit', header: { text: '부과량\n(천톤)' }, items: ['LEVY_QY'] }] - }, - { - name: 'homeGroup', header: { text: '가 정 용' }, - items: [{ name: 'hAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['HOME_LEVY_AMOUNT'] }, { name: 'hQyUnit', header: { text: '부과량\n(천톤)' }, items: ['HOME_LEVY_QY'] }, { name: 'hUntUnit', header: { text: '단가\n(원/톤)' }, items: ['HOME_UNTPC'] }] - }, - { - name: 'jobGroup', header: { text: '업 무 용' }, - items: [{ name: 'jAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['JOB_LEVY_AMOUNT'] }, { name: 'jQyUnit', header: { text: '부과량\n(천톤)' }, items: ['JOB_LEVY_QY'] }, { name: 'jUntUnit', header: { text: '단가\n(원/톤)' }, items: ['JOB_UNTPC'] }] - }, - { - name: 'bsnGroup', header: { text: '영 업 용(일 반 용)' }, - items: [{ name: 'bAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BSN_LEVY_AMOUNT'] }, { name: 'bQyUnit', header: { text: '부과량\n(천톤)' }, items: ['BSN_LEVY_QY'] }, { name: 'bUntUnit', header: { text: '단가\n(원/톤)' }, items: ['BSN_UNTPC'] }] - }, - { - name: 'bath1Group', header: { text: '욕 탕 1종(대중탕용)' }, - items: [{ name: 'bt1AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS1_LEVY_AMOUNT'] }, { name: 'bt1QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS1_LEVY_QY'] }, { name: 'bt1UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS1_UNTPC'] }] - }, - { - name: 'bath2Group', header: { text: '욕 탕 2종' }, - items: [{ name: 'bt2AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS2_LEVY_AMOUNT'] }, { name: 'bt2QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS2_LEVY_QY'] }, { name: 'bt2UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS2_UNTPC'] }] - }, - { - name: 'industGroup', header: { text: '산업용 및 공업용' }, - items: [{ name: 'iAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['INDUST_LEVY_AMOUNT'] }, { name: 'iQyUnit', header: { text: '부과량\n(천톤)' }, items: ['INDUST_LEVY_QY'] }, { name: 'iUntUnit', header: { text: '단가\n(원/톤)' }, items: ['INDUST_UNTPC'] }] - }, - { - name: 'etcGroup', header: { text: '기타' }, - items: [{ name: 'eAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['ETC_LEVY_AMOUNT'] }, { name: 'eQyUnit', header: { text: '부과량\n(천톤)' }, items: ['ETC_LEVY_QY'] }, { name: 'eUntUnit', header: { text: '단가\n(원/톤)' }, items: ['ETC_UNTPC'] }] - }, - 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '원인자부담금', - rowStartNum: 3, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'LEVY_CO', fieldName: '건수(부과액)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '금액(백만원)(부과액)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'TRGET_LEV_STTUS_CO', fieldName: '건수(계)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TRGET_LEV_STTUS_AMOUNT', fieldName: '금액(백만원)(계)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRNG_EQP_CO', fieldName: '건수(배수설비)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DRNG_EQP_AMOUNT', fieldName: '금액(백만원)(배수설비)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CNTRWK_CO', fieldName: '건수(타공사)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CNTRWK_AMOUNT', fieldName: '금액(백만원)(타공사)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { name: 'levyMainGroup', header: { text: '부과액' }, items: ['LEVY_CO', 'LEVY_AMOUNT'] }, - { - name: 'collectGroup', header: { text: '대상별 징수현황' }, - items: [ - { name: 'collectTotal', header: { text: '계' }, items: ['TRGET_LEV_STTUS_CO', 'TRGET_LEV_STTUS_AMOUNT'] }, - { name: 'contractorGroup', header: { text: '배수설비설치자\n(하수도법 제61조 제1항)' }, items: ['DRNG_EQP_CO', 'DRNG_EQP_AMOUNT'] }, - { name: 'otherGroup', header: { text: '타공사·타행위자\n(하수도법 제61조 제2항)' }, items: ['CNTRWK_CO', 'CNTRWK_AMOUNT'] } - ] - }, - 'RMK' - ] + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const SewerageFinanceInfoListRvw = rawFinanceList.map(tab => ({ +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "행정구역명" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + "admdstNm", + ], +}; + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 09. 하수도 재정 +const rawFinanceList = [ + { + tabKey: "ST00090001", + tabLabel: "하수도재정 결산", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + decimal1Col("fnnByWholPjtcoAmt", "계"), + decimal1Col("cnterGovSprtAmt", "중앙정부\n지원액"), + decimal1Col("mctpvLcexpAmt", "특․광역시, 도"), + decimal1Col("wareaSggLclAmt", "시,군,구 지자체"), + decimal1Col("sewerUseAmt", "하수도\n사용료"), + decimal1Col("orgprBrdnAmt", "원인자\n부담금"), + decimal1Col("etcIncmAmt", "기타\n잡수입"), + decimal1Col("crfwAmt", "이월액"), + decimal1Col("lbltAmt", "부채액\n(백만원)"), + decimal1Col("usgByWholPjtcoAmt", "계"), + decimal1Col("tmplntWholAmt", "소계"), + decimal1Col("tmplntFcltAmt", "시설비"), + decimal1Col("tmplntMendngAmt", "개보수비"), + decimal1Col("tmplntOperKeepAmt", "운영\n유지비"), + decimal1Col("tmplntEtcAmt", "기타"), + decimal1Col("wholSwagePplAmt", "소계"), + decimal1Col("swagePplFcltAmt", "시설비"), + decimal1Col("swagePplMendngAmt", "개보수비"), + decimal1Col("swagePplOperKeepAmt", "운영\n유지비"), + decimal1Col("swagePplEtcAmt", "기타"), + decimal1Col("wholSwageImpdFcltAmt", "소계"), + decimal1Col("swageImpdFcltAmt", "시설비"), + decimal1Col("swageImpdMendngAmt", "개보수비"), + decimal1Col("swageImpdOperKeepAmt", "운영\n유지비"), + decimal1Col("swageImpdFcltEtcAmt", "기타"), + decimal1Col("etcWholAmt", "소계"), + decimal1Col("etcAcdntCrfwAmt", "사고\n이월액"), + decimal1Col("etcNrcpAmt", "미수납액"), + decimal1Col("etcAmt", "기타"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { + name: "financeGroup", + header: { text: "재정별 사업비(백만원)" }, + items: [ + "fnnByWholPjtcoAmt", + "cnterGovSprtAmt", + { + name: "localGroup", + header: { text: "지방비" }, + items: ["mctpvLcexpAmt", "wareaSggLclAmt"], + }, + "sewerUseAmt", + "orgprBrdnAmt", + "etcIncmAmt", + "crfwAmt", + ], + }, + "lbltAmt", + { + 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: [ + "wholSwageImpdFcltAmt", + "swageImpdFcltAmt", + "swageImpdMendngAmt", + "swageImpdOperKeepAmt", + "swageImpdFcltEtcAmt", + ], + }, + { + name: "t4Group", + header: { text: "기타" }, + items: ["etcWholAmt", "etcAcdntCrfwAmt", "etcNrcpAmt", "etcAmt"], + }, + ], + }, + "rmrk", + ], + }, + { + tabKey: "ST00090002", + tabLabel: "하수도 요금", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("anlLevyQnt", "연간조정량(㎡)"), + intCol("levyAmt", "하수도사용료수익(원)"), + decimal1Col("avgUntprcAmt", "㎡당 요금(원/㎡)"), + intCol("grssPrmcoAmt", "총관원가(원)"), + decimal1Col("grssUnitPrmcoAmt", "㎡당원가(원/㎡)"), + { + name: "rlztnRt", + fieldName: "rlztnRt", + header: { text: "현실화율(%)" }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "anlLevyQnt", + "levyAmt", + "avgUntprcAmt", + "grssPrmcoAmt", + "grssUnitPrmcoAmt", + "rlztnRt", + "rmrk", + ], + }, + { + tabKey: "ST00090003", + tabLabel: "하수도 사용료", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + decimal1Col("levyAmt", "부과액(백만원)"), + decimal1Col("levyQnt", "부과량(천톤)"), + decimal1Col("hshusLevyAmt", "부과액"), + decimal1Col("hshusLevyQnt", "부과량"), + intCol("hshusUntprcAmt", "단가"), + decimal1Col("taskFrLevyAmt", "부과액"), + decimal1Col("taskFrLevyQnt", "부과량"), + intCol("taskFrUntprcAmt", "단가"), + decimal1Col("bsnGnrlLevyAmt", "부과액(백만원)"), + decimal1Col("bsnGnrlLevyQnt", "부과량(천톤)"), + intCol("bsnGnrlUntprcAmt", "단가(원/톤)"), + decimal1Col("massBthhsFrLevyAmt", "부과액(백만원)"), + decimal1Col("massBthhsFrLevyQnt", "부과량(천톤)"), + intCol("massBthhsFrUntprcAmt", "단가(원/톤)"), + decimal1Col("bthhsFrSpcs2LevyAmt", "부과액"), + decimal1Col("bthhsFrSpcs2LevyQnt", "부과량"), + intCol("bthhsFrSpcs2UntprcAmt", "단가"), + decimal1Col("induMdsrLevyAmt", "부과액(백만원)"), + decimal1Col("induMdsrLevyQnt", "부과량(천톤)"), + intCol("induMdsrUntprcAmt", "단가(원/톤)"), + decimal1Col("etcLevyAmt", "부과액"), + decimal1Col("etcLevyQnt", "부과량"), + intCol("etcLevyUntprcAmt", "단가"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { 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"], + }, + "rmrk", + ], + }, + { + tabKey: "ST00090004", + tabLabel: "원인자부담금", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("levyEvntcsCnt", "건수"), + decimal1Col("levyAmt", "금액(백만원)"), + intCol("trgtByWholClctnEvntcsCnt", "건수"), + decimal1Col("trgtByWholClctnAmt", "금액(백만원)"), + intCol("mltvEqpmEvntcsCnt", "건수"), + decimal1Col("mltvEqpmAmt", "금액(백만원)"), + intCol("othrCstrnOthrActrCnt", "건수"), + decimal1Col("othrCstrnOthrActrAmt", "금액(백만원)"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { 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"], + }, + ], + }, + "rmrk", + ], + }, +]; + +export const SewerageFinanceInfoListRvw = rawFinanceList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts b/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts index c673bbda5..316825d5b 100644 --- a/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts @@ -1,117 +1,213 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 13. 하수도 서비스 수준 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + // { + // name: "admdstNm", + // fieldName: "admdstNm", + // header: { text: "행정구역명" }, + // mergeCells: true, + // sortdisable: true, + // }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + // "admdstNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const avgDecimalCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +// 하수도 통계 검토 13. 하수도 서비스 수준 const rawServiceLevelList = [ { - tabKey: 'a', - tabLabel: '민원', + tabKey: "ST00130001", + tabLabel: "민원", rowStartNum: 2, fixedColumns: 2, columns: [ - /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } }, - - /* 2. 하수처리 인구 (정수 반영) */ - { name: 'POPLTN_SM', fieldName: 'POPLTN_SM', header: { text: '하수처리인구\n(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 3. 민원유형 (단위: 건 / 정수 반영) */ - { name: 'CVPL_CM', fieldName: 'CVPL_CM', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: 'CVPL_DCWTR_QLTWTR', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: 'CVPL_BSML', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: 'CVPL_FIVW', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: 'CVPL_POLLUTN_ACDNT', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: 'CVPL_SSOS', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: 'CVPL_CHRGE', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_ETC', fieldName: 'CVPL_ETC', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 성과 지표 및 비고 (소수점 1자리 반영) */ - { name: 'STDR_POPLTN_PER_CVPL', fieldName: 'STDR_POPLTN_PER_CVPL', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: 'RMK', header: { text: '비고' } } + ...reviewAddressColumns, + intCol("swtrmtPpltnCnt", "하수처리인구\n(명)"), + intCol("whol", "총계"), + intCol("dschgWqCnt", "악취"), + intCol("odorCnt", "하수관로"), + intCol("ldsCnt", "배수설비"), + intCol("plltnAcdntCnt", "맨홀"), + intCol("swagePplCnt", "정화조"), + intCol("sewerCrgAmt", "하수도요금"), + intCol("etcCnt", "기타"), + avgDecimalCol("crtrPpltnCvlcptEvntcsCnt", "기준인구당\n민원건수\n(건/10,000명)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - 'POPLTN_SM', + reviewAddressLayout, + "displayYear", + "swtrmtPpltnCnt", { - name: 'complaintTypeGroup', - header: { text: '민원유형(건)' }, + name: "complaintTypeGroup", + header: { text: "민원유형(건)" }, items: [ - 'CVPL_CM', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_BSML', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_ETC' - ] + "whol", + "dschgWqCnt", + "odorCnt", + "ldsCnt", + "plltnAcdntCnt", + "swagePplCnt", + "sewerCrgAmt", + "etcCnt", + ], }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' - ] + "crtrPpltnCvlcptEvntcsCnt", + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '주민친화시설', + tabKey: "ST00130002", + tabLabel: "주민친화시설", rowStartNum: 2, - fixedColumns: 5, + fixedColumns: 2, columns: [ - /* 1. 기본 및 시설 정보 */ - { name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: 'BSC_FCT_NM', header: { text: '처리장명' } }, - { name: 'FCT_CPC', fieldName: 'FCT_CPC', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } }, - - /* 2. 처리장 전체 면적 (소수점 1자리 반영) */ - { name: 'PRCPLT_AR', fieldName: 'PRCPLT_AR', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 주민친화시설 면적 상세 (단위: ㎡ / 정수 반영) */ - { name: 'TOTAL_AR', fieldName: 'TOTAL_AR', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: 'RESTRM_AR', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: 'CHILDPLAY_AR', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: 'ALSFC_AR', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: 'PLGRND_AR', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: 'PRKPLCE_AR', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: 'CTLSTT_AR', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: 'NRPRK_AR', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: 'ETC_AR', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 비고 */ - { name: 'RMK', fieldName: 'RMK', header: { text: '비고' } } + ...reviewAddressColumns, + textCol("swtrmtFcltNm", "처리장명"), + intCol("fcltCpct", "시설용량\n(㎥/일)"), + decimal1Col("tmplntArea", "처리장\n면적(㎡)"), + decimal1Col("whol", "총계"), + decimal1Col("brkFcltArea", "휴게시설"), + decimal1Col("childFrshpSpacArea", "어린이놀이공간"), + decimal1Col("bdySportsFcltArea", "체력단련시설"), + decimal1Col("sportsFcltArea", "운동시설"), + decimal1Col("pkltArea", "주차장"), + decimal1Col("eduMultiFcltArea", "교육 및\n문화시설"), + decimal1Col("wtlndMakeArea", "습지조성\n(생태공원)"), + decimal1Col("etcCnt", "기타"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'YR_GBN', - 'PRCPLT_AR', + reviewAddressLayout, + "displayYear", + "swtrmtFcltNm", + "fcltCpct", + "tmplntArea", { - name: 'friendlyFacilityGroup', - header: { text: '주민친화시설 면적(㎡)' }, + name: "friendlyFacilityGroup", + header: { text: "주민친화시설 면적(㎡)" }, items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' - ] + "whol", + "brkFcltArea", + "childFrshpSpacArea", + "bdySportsFcltArea", + "sportsFcltArea", + "pkltArea", + "eduMultiFcltArea", + "wtlndMakeArea", + "etcCnt", + ], }, - 'RMK' - ] - } + "rmrk", + ], + }, ]; -export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map(tab => ({ +export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewerageServiceLevelInfoListRvw` 사용 */ +export const SewerageServiceLevelInfoList = SewerageServiceLevelInfoListRvw; diff --git a/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts b/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts index 04026197f..2d045707d 100644 --- a/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts +++ b/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts @@ -1,224 +1,366 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 10. 물 재이용 -const rawWaterReuseList = [ - { - tabKey: 'a', - tabLabel: '공공하수처리수 재이용', - rowStartNum: 4, - fixedColumns: 2, - columns: [ - /* 1. 기본 및 행정 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerDecimal2ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; - /* 2. 하수처리장 기본 정보 */ - { name: 'BSC_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'EQP_PRMS_DT', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'TPUT_TOTAL', fieldName: '연간 하수처리량(천톤/년)', header: { text: '연간\n하수처리량\n(천톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_LAW_YN', fieldName: '법적의무대상(대상/미대상)', header: { text: '법적의무대상\n(대상/미대상)' } }, +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 3. 재이용시설 현황 */ - { name: 'INST_YN', fieldName: '설치유무(유/무)', header: { text: '설치유무\n(유/무)' } }, - { name: 'RSWTR_RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } }, - { name: 'RSWTR_PRMS_DT', fieldName: '설치승인일 yyyy-mm-dd', header: { text: '설치승인일\nyyyy-mm-dd' } }, - { name: 'RSWTR_CPC', fieldName: '용량(톤/일)', header: { text: '용량(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_TRT_NM', fieldName: '공법/기자재', header: { text: '공법/기자재' } }, - { name: 'RSWTR_PIPE', fieldName: '공급관로길이(km)', header: { text: '공급관로길이\n(km)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_PUMP', fieldName: '공급펌프(톤/일)', header: { text: '공급펌프\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } - /* 4. 재이용 실적 - 총계 및 장내용수 */ - { name: 'USE_AMNT_TOTAL', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_D01', fieldName: '계(장내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S01', fieldName: '세척수', header: { text: '세척수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S02', fieldName: '냉각수', header: { text: '냉각수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S03', fieldName: '청소수', header: { text: '청소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S04', fieldName: '조경용수(장내)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S05', fieldName: '희석용수', header: { text: '희석용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S06', fieldName: '장내중수도', header: { text: '장내중수도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S07', fieldName: '장내기타용수', header: { text: '장내기타용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } - /* 5. 재이용 실적 - 장외용수 */ - { name: 'USE_AMNT_D02', fieldName: '계(장외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S11', fieldName: '청소·화장실 용수', header: { text: '청소·화장실\n용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S12', fieldName: '세척·살수용수', header: { text: '세척·살수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S13', fieldName: '조경용수(장외)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S14', fieldName: '친수용수', header: { text: '친수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S15', fieldName: '하천등 유지용수', header: { text: '하천등 유지용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S16', fieldName: '농업용수', header: { text: '농업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S17', fieldName: '지하수충전', header: { text: '지하수충전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S18', fieldName: '공업용수', header: { text: '공업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; - /* 6. 지표 및 비고 */ - { name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { - name: 'sewagePlantGroup', - header: { text: '하수처리장' }, - items: ['BSC_FCT_NM', 'EQP_PRMS_DT', 'FCT_CPC', 'TPUT_TOTAL', 'RSWTR_LAW_YN'] - }, - { - name: 'reuseFctGroup', - header: { text: '재이용시설' }, - items: [ - 'INST_YN', - { - name: 'installDetailGroup', - header: { text: '재이용시설 설치시' }, - items: [ - 'RSWTR_RN_DT', - 'RSWTR_PRMS_DT', - { - name: 'reProcessGroup', - header: { text: '재처리시설' }, - items: ['RSWTR_CPC', 'RSWTR_TRT_NM'] - }, - 'RSWTR_PIPE', - 'RSWTR_PUMP' - ] - } - ] - }, - { - name: 'reuseStatusGroup', - header: { text: '하수처리수 재이용현황(천톤/년)' }, - items: [ - 'USE_AMNT_TOTAL', - { - name: 'internalUseGroup', - header: { text: '장 내 용 수' }, - items: [ - 'USE_AMNT_D01', 'USE_AMNT_S01', 'USE_AMNT_S02', 'USE_AMNT_S03', - 'USE_AMNT_S04', 'USE_AMNT_S05', 'USE_AMNT_S06', 'USE_AMNT_S07' - ] - }, - { - name: 'externalUseGroup', - header: { text: '장 외 용 수' }, - items: [ - 'USE_AMNT_D02', 'USE_AMNT_S11', 'USE_AMNT_S12', 'USE_AMNT_S13', - 'USE_AMNT_S14', 'USE_AMNT_S15', 'USE_AMNT_S16', 'USE_AMNT_S17', 'USE_AMNT_S18' - ] - } - ] - }, - 'REUSE_RT', - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '중수도 현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BILD_NM', fieldName: '건축물명/개발사업명', header: { text: '건축물명/\n개발사업명' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'BILD_TP', fieldName: '시설물용도/개발사업종류', header: { text: '시설물용도/\n개발사업종류' } }, - { name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } }, - { name: 'COMP_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } }, - { name: 'RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } }, - { name: 'TOTAR', fieldName: '건축연면적(㎡)', header: { text: '건축연면적\n(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PROC_CPCTY', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WATER_USE_CPCTY', fieldName: '물 사용량(㎥/년)', header: { text: '물 사용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSWTR_USE_SM', fieldName: '중수도 이용량(㎥/년)', header: { text: '중수도\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SUB_USE_CPCTY', fieldName: '대체수 이용량(㎥/년)', header: { text: '대체수\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSWTR_PRPOS', fieldName: '중수도용도', header: { text: '중수도용도' } }, - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BILD_NM', 'ADDR', 'YR_GBN', 'BILD_TP', 'LAW_YN', 'COMP_DT', 'RN_DT', - { - name: 'fctStatusGroup', - header: { text: '시설현황' }, - items: ['TOTAR', 'PROC_CPCTY'] - }, - 'WATER_USE_CPCTY', 'RUSWTR_USE_SM', 'SUB_USE_CPCTY', 'RUSWTR_PRPOS', 'FYER_OPER_CT', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '빗물이용시설 현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'RWR_REUSE_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'BILD_TP', fieldName: '건축물용도', header: { text: '건축물용도' } }, - { name: 'ADDR', fieldName: '위치(주소)', header: { text: '위치\n(주소)' } }, - { name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } }, - { name: 'RN_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } }, - { name: 'INSTL_CT', fieldName: '설치비(백만원)', header: { text: '설치비\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CCM', fieldName: '집수면 종류', header: { text: '집수면 종류' } }, - { name: 'TRTFC_ENNC', fieldName: '여과등 처리시설 유무', header: { text: '여과등\n처리시설\n유무' } }, - { name: 'SGBS_CPCTY', fieldName: '저류조 용량(㎥)', header: { text: '저류조 용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'FYER_USEQTY', fieldName: '연간 이용량(㎥/년)', header: { text: '연간 이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RWR_PRCUSE_PRPPOS', fieldName: '이용용도', header: { text: '이용용도' } }, - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'RWR_REUSE_FCT_NM', 'YR_GBN', 'BILD_TP', 'ADDR', 'LAW_YN', 'RN_DT', 'INSTL_CT', 'CCM', 'TRTFC_ENNC', 'SGBS_CPCTY', 'FYER_USEQTY', 'RWR_PRCUSE_PRPPOS', 'FYER_OPER_CT', 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 등록현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'CMPN_RGSDE', fieldName: '등록일 yyyy-mm-dd', header: { text: '등록일\nyyyy-mm-dd' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK' - ] - }, - { - tabKey: 'e', - tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 인가현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'CMPN_RGSDE', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK' - ] - } +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const WaterReuseInfoList = rawWaterReuseList.map(tab => ({ - ...tab, - columns: tab.columns.map(col => ({ - ...col, - editable: false - })) -})); \ No newline at end of file +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewFacilityAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewFacilityAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + "sggNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const decimal2Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal2ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 10. 물 재이용 +const rawWaterReuseList = [ + { + tabKey: "ST00100001", + tabLabel: "공공하수처리수 재이용", + rowStartNum: 4, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("swtrmtFcltNm", "시설명"), + textCol("sncnYmd", "인가일\n(년/월/일)"), + decimal1Col("prcsFcltCpct", "시설용량\n(㎥/일)"), + decimal1Col("prcsQnt", "연간\n하수처리량\n(천톤/년)"), + textCol("lglYn", "법적의무대상\n(대상/미대상)"), + textCol("instlYn", "설치유무\n(유/무)"), + textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"), + textCol("instlAprvYmd", "설치승인일\nyyyy-mm-dd"), + decimal1Col("rprcsFcltCpct", "용량(톤/일)"), + textCol("mtcnsNm", "공법/기자재"), + decimal2Col("ruswtPplLen", "공급관로길이\n(km)"), + decimal1Col("ruswtPumpLen", "공급펌프\n(톤/일)"), + decimal1Col("wholUseQnt", "총계"), + decimal1Col("ithlWshwtrQnt", "계"), + decimal1Col("ithlClnQty", "세척수"), + decimal1Col("ithlFrzgQty", "냉각수"), + decimal1Col("ithlLdhcUswtrQnt", "청소수"), + decimal1Col("ithlDltnUswtrQnt", "조경용수"), + decimal1Col("ithlRuswtrQnt", "희석용수"), + decimal1Col("ithlEtcQnt", "장내중수도"), + decimal1Col("ithlUnsdQnt", "장내기타용수"), + decimal1Col("otcntrUswtrWhol", "계"), + decimal1Col("otcntrClnRstrmQnt", "청소·화장실\n용수"), + decimal1Col("otcntrWshwtrQnt", "세척·살수용수"), + decimal1Col("otcntrLdhcUswtrQnt", "조경용수"), + decimal1Col("otcntrHdrplUswtrQnt", "친수용수"), + decimal1Col("otcntrRvrKeepQnt", "하천등 유지용수"), + decimal1Col("agrcltUswtrPstnQnt", "농업용수"), + decimal1Col("ugwtrChargQnt", "지하수충전"), + decimal1Col("mdsrUswtrQnt", "공업용수"), + { + name: "rtlztnRt", + fieldName: "rtlztnRt", + header: { text: "재이용률\n(%)" }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + { + 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", + "rmrk", + ], + }, + { + tabKey: "ST00100002", + tabLabel: "중수도 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("fcltNm", "건축물명/\n개발사업명"), + textCol("bldgSeCn", "시설물용도/\n개발사업종류"), + textCol("fcltPstnCn", "주소"), + textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"), + textCol("cmptnStrtYmd", "설치완료일\nyyyy-mm-dd"), + textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"), + intCol("archWholArea", "건축연면적\n(㎡)"), + decimal1Col("prcsCpct", "처리용량\n(㎥/일)"), + intCol("wtrUseQnt", "물 사용량\n(㎥/년)"), + intCol("ruswtrUtztnQnt", "중수도\n이용량\n(㎥/년)"), + intCol("sbstCntUtztnQnt", "대체수\n이용량\n(㎥/년)"), + textCol("mnUsgCn", "중수도용도"), + decimal1Col("ruswtrOperAmt", "연간운영비\n(백만원/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "bldgSeCn", + "fcltPstnCn", + "lglTrgtYn", + "cmptnStrtYmd", + "oprtngStrtYmd", + { + name: "facilityStatusGroup", + header: { text: "시설현황" }, + items: ["archWholArea", "prcsCpct"], + }, + "wtrUseQnt", + "ruswtrUtztnQnt", + "sbstCntUtztnQnt", + "mnUsgCn", + "ruswtrOperAmt", + "rmrk", + ], + }, + { + tabKey: "ST00100003", + tabLabel: "빗물이용시설 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("fcltNm", "시설명"), + textCol("bdstTypCn", "건축물용도"), + textCol("fcltPstnCn", "위치\n(주소)"), + textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"), + textCol("instlCmptnYmd", "설치완료일\nyyyy-mm-dd"), + decimal1Col("instlAmt", "설치비\n(백만원)"), + textCol("rcptMynCn", "집수면 종류"), + textCol("filtrPrcsFcltYnExpln", "여과등\n처리시설\n유무"), + intCol("impdCpct", "저류조 용량\n(㎥)"), + intCol("anlUseQnt", "연간 이용량\n(㎥/년)"), + textCol("exlnUtlzUsgExpln", "이용용도"), + decimal1Col("anlOperAmt", "연간운영비\n(백만원/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "bdstTypCn", + "fcltPstnCn", + "lglTrgtYn", + "instlCmptnYmd", + "instlAmt", + "rcptMynCn", + "filtrPrcsFcltYnExpln", + "impdCpct", + "anlUseQnt", + "exlnUtlzUsgExpln", + "anlOperAmt", + "rmrk", + ], + }, + { + tabKey: "ST00100004", + tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 등록현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("bplcNm", "상호(명칭)"), + textCol("rprsvNm", "대표자"), + textCol("bplcPstnCn", "주소"), + textCol("regDt", "등록일\nyyyy-mm-dd"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "regDt", + "rmrk", + ], + }, + { + tabKey: "ST00100005", + tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 인가현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("bplcNm", "상호(명칭)"), + textCol("rprsvNm", "대표자"), + textCol("bplcPstnCn", "주소"), + textCol("coRegYmd", "인가일\n(년/월/일)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "coRegYmd", + "rmrk", + ], + }, +]; + +export const WaterReuseInfoListRvw = rawWaterReuseList.map((tab) => ({ + ...tab, + columns: tab.columns.map((col) => ({ + ...col, + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `WaterReuseInfoListRvw` 사용 */ +export const WaterReuseInfoList = WaterReuseInfoListRvw; diff --git a/apps/st/src/components/util/grid/gridFunction.ts b/apps/st/src/components/util/grid/gridFunction.ts index 28755a7f8..09f88fde9 100644 --- a/apps/st/src/components/util/grid/gridFunction.ts +++ b/apps/st/src/components/util/grid/gridFunction.ts @@ -1,3 +1,13 @@ +import type { CellStyleCallback } from "@/components/type/DataGrid.type"; + +/** 검토(review) 그리드: rowType이 CURR인 행(당해 연도) 흰색 배경 */ +export const reviewRowTypeCurrCellStyleCallback: CellStyleCallback = ({ row }) => { + if (String(row.rowType ?? "") === "CURR") { + return "rg-cell-rowtype-curr"; + } + return undefined; +}; + /** * 객체 배열을 그리드 row 형식으로 변환 (null/undefined → ""). * 어떤 엔티티 타입이든 공통 사용 가능. diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts index 4622da5c8..87ec895be 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts @@ -29,6 +29,8 @@ import { type TreatWaterReuseDesignConstStatusSaveResponse, type TreatWaterReuseDesignConstStatusSubmitRequest, type TreatWaterReuseDesignConstStatusSubmitResponse, + type WaterReuseOperationDataUpdateRequest, + type WaterReuseOperationDataUpdateResponse, type WaterReuseRequestParams, } from "../../model/input/statistics-input-water-reuse-interface"; @@ -199,6 +201,17 @@ export class StatisticsInputWaterReuseApiService implements StatisticsInputWater ); return response.data; } + + /** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */ + public async updateWaterReuseOperationData( + request: WaterReuseOperationDataUpdateRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/operation-data/update`, + request, + ); + return response.data; + } } export const statisticsInputWaterReuseApi = new StatisticsInputWaterReuseApiService(); 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 b0cd77d3a..bc81d6a8b 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 @@ -9,6 +9,8 @@ import type { PublicSewageFacilitySaveResponse, PublicSewageFacilitySubmitRequest, PublicSewageFacilitySubmitResponse, + PublicSewageFacilityOperationDataUpdateRequest, + PublicSewageFacilityOperationDataUpdateResponse, PublicSewageMainFacilityMappingSaveRequest, PublicSewageMainFacilityMappingSaveResponse, PublicSewageMainFacilityResponse, @@ -73,6 +75,16 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics ); return response.data; } + + public async updatePublicSewageFacilityOperationData( + param: PublicSewageFacilityOperationDataUpdateRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/operation-data/update`, + param, + ); + return response.data; + } } export const statisticsInputPublicSewageFacilityApiService = new StatisticsInputPublicSewageFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-management-service.ts b/apps/st/src/entities/statistics/api/statistics-management-service.ts index 9bb1156e6..d7d2f052f 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -35,6 +35,34 @@ import { type PublicSewageFacilityReviewApiResponse, type PublicSewageFacilityReviewRequestParams, } from "../model/statistics-public-sewage-facility-review-interface"; +import { + type ExcretaTreatmentFacilityReviewApiResponse, + type ExcretaTreatmentFacilityReviewRequestParams, +} from "../model/statistics-excreta-treatment-facility-review-interface"; +import { + type PrivateSewageFacilityReviewApiResponse, + type PrivateSewageFacilityReviewRequestParams, +} from "../model/statistics-private-sewage-facility-review-interface"; +import { + type SewerageFinanceReviewApiResponse, + type SewerageFinanceReviewRequestParams, +} from "../model/statistics-sewerage-finance-review-interface"; +import { + type WaterReuseReviewApiResponse, + type WaterReuseReviewRequestParams, +} from "../model/statistics-water-reuse-review-interface"; +import { + type SewageExcretaSlurgeReviewApiResponse, + type SewageExcretaSlurgeReviewRequestParams, +} from "../model/statistics-sewage-excreta-slurge-review-interface"; +import { + type SewageExcretaRelatedReviewApiResponse, + type SewageExcretaRelatedReviewRequestParams, +} from "../model/statistics-sewage-excreta-related-review-interface"; +import { + type SewerageServiceLevelReviewApiResponse, + type SewerageServiceLevelReviewRequestParams, +} from "../model/statistics-sewerage-service-level-review-interface"; export class StatisticsManagementApiService { @@ -80,6 +108,20 @@ export class StatisticsManagementApiService { return response.data; } + /** + * 07. 분뇨처리시설 검토 목록 조회 + * @param params (sido, gugun, yr, bsnmCd, fctName) + */ + public async getExcretaTreatmentFacilityReviewList( + params: ExcretaTreatmentFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/excreta-treatment`, + { params }, + ); + return response.data; + } + /** * 02. 하수관로 지역별 시설현황 목록 조회 (Tab1) * @param params (sido, gugun, yr, bsnmCd) @@ -226,6 +268,292 @@ export class StatisticsManagementApiService { return response.data; } + /** + * 08. 개인하수처리시설 tab1. 처리용량별 오수처리시설 현황 검토 조회 — ST00080001 + */ + public async getPrivateSewageTreatFactStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-sevage`, + { params }, + ); + return response.data; + } + + /** + * 08. 개인하수처리시설 tab2. 정화조 현황 검토 조회 — ST00080002 + */ + public async getSepticTankStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-tank-status`, + { params }, + ); + return response.data; + } + + /** + * 08. 개인하수처리시설 tab3. 개인하수도 관리지역 지정공고 검토 조회 — ST00080003 + */ + public async getPrivateSewageDesigStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-mgmt-area`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab1. 하수도재정 결산 검토 조회 — ST00090001 + */ + public async getSewerageFinanceSettleReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-financial`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab2. 하수도 요금 검토 조회 — ST00090002 + */ + public async getSewerageChargeReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-fee`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab3. 하수도 사용료 검토 조회 — ST00090003 + */ + public async getSewerageUsageFeeReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-usage`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab4. 원인자부담금 검토 조회 — ST00090004 + */ + public async getCauserBurdenReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-org-cause`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab1. 공공하수처리수 재이용 검토 조회 — ST00100001 + */ + public async getWaterReuseReuseStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/pbl-swage-ruswt`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab2. 중수도 현황 검토 조회 — ST00100002 + */ + public async getWaterReuseGrayWaterReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/gray-water-status`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab3. 빗물이용시설 현황 검토 조회 — ST00100003 + */ + public async getWaterReuseRainwaterReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/water-reuse`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab4. 하·폐수처리수 재이용시설 시공업 등록현황 검토 조회 — ST00100004 + */ + public async getWaterReuseRegStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-reg`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab5. 하·폐수처리수 재이용시설 시공업 인가현황 검토 조회 — ST00100005 + */ + public async getWaterReuseSncnStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-aprv`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab1. 찌꺼기(슬러지)처리시설현황 검토 조회 — ST00110001 + */ + public async getSludgeProcessingReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-processing`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab2. 소화조 운영현황 검토 조회 — ST00110002 + */ + public async getSludgeDigestionTankReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-digestion-tank`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab3. 하수찌꺼기 처리시설 운영현황 검토 조회 — ST00110003 + */ + public async getSludgeFacilityOperReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-facility-oper`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab4. 분뇨찌꺼기(슬러지)처리현황 검토 조회 — ST00110004 + */ + public async getHumanWasteSludgeReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-human-waste`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab1. 공공기관 하수분야 검토 조회 — ST00120001 + */ + public async getPublicSewageStaffReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/public-sewage-staff`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab2. 공공기관 분뇨분야 검토 조회 — ST00120002 + */ + public async getPublicHumanWasteStaffReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/public-human-waste-staff`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab3. 분뇨수집·운반업 검토 조회 — ST00120003 + */ + public async getHumanWasteCollectionReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/human-waste-collection`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab4. 개인하수처리관련업 검토 조회 — ST00120004 + */ + public async getIndvSwtrmtRelBsnsReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/indv-swtrmt-rel-bsns`, + { params }, + ); + return response.data; + } + + /** + * 13. 하수도 서비스 수준 tab1. 민원 검토 조회 — ST00130001 + */ + public async getSewageComplaintReviewList( + params: SewerageServiceLevelReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewage-complaint`, + { params }, + ); + return response.data; + } + + /** + * 13. 하수도 서비스 수준 tab2. 주민친화시설 검토 조회 — ST00130002 + */ + public async getResidentFriendlyFacilityReviewList( + params: SewerageServiceLevelReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/resident-friendly-facility`, + { params }, + ); + return response.data; + } + public async getReviewHistory(params: ReviewHistoryRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/history`, diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index f8171d885..23a1bd394 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -1,4 +1,5 @@ import { client, stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import { @@ -310,7 +311,7 @@ export class StatisticsApiService implements StatisticsService { public async getReviewerInfo(param: ReviewerInfoRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/reviewer-info`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } 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 9109f76e2..e2b78f8df 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 @@ -46,15 +46,15 @@ export interface SewageReservoirItem { wtrStrgZoneArea: number; impdFcltCpct: number; grssPpqty: number; - pumpFcltN1DmtrExpln: string; + pumpFcltN1DmtrExpln: number; pumpFcltN1PerPpqty: number; pumpFcltN1PumpCnt: number; pumpFcltN1RsrvPumpCnt: number; - pumpFcltN2DmtrExpln: string; + pumpFcltN2DmtrExpln: number; pumpFcltN2PerPpqty: number; pumpFcltN2PumpCnt: number; pumpFcltN2RsrvPumpCnt: number; - pumpFcltN3DmtrExpln: string; + pumpFcltN3DmtrExpln: number; pumpFcltN3PerPpqty: number; pumpFcltN3PumpCnt: number; pumpFcltN3RsrvPumpCnt: number; @@ -114,17 +114,17 @@ export interface RainfallPumpStationItem { abstnRgnImpdQnt: number; grssPpqty: number; - pumpFcltN1DmtrExpln: string; + pumpFcltN1DmtrExpln: number; pumpFcltN1PerPpqty: number; pumpFcltN1PumpCnt: number; pumpFcltN1RsrvPumpCnt: number; - pumpFcltN2DmtrExpln: string; + pumpFcltN2DmtrExpln: number; pumpFcltN2PerPpqty: number; pumpFcltN2PumpCnt: number; pumpFcltN2RsrvPumpCnt: number; - pumpFcltN3DmtrExpln: string; + pumpFcltN3DmtrExpln: number; pumpFcltN3PerPpqty: number; pumpFcltN3PumpCnt: number; pumpFcltN3RsrvPumpCnt: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts index e3eb5a16c..87cf5addf 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts @@ -520,6 +520,20 @@ export interface TreatWaterReuseBusinessApprStatusSubmitResponse { }; } +export interface WaterReuseOperationDataUpdateRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; +} + +export interface WaterReuseOperationDataUpdateResponse { + data?: string; + message?: string; + status: string; +} + export interface StatisticsInputWaterReuseService { /** 공공하수처리수 재이용 조회 — ST00100001 */ getPublicSewageTreatWaterReuseList( @@ -581,4 +595,8 @@ export interface StatisticsInputWaterReuseService { submitTreatWaterReuseBusinessApprStatus( request: TreatWaterReuseBusinessApprStatusSubmitRequest, ): Promise; + /** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */ + updateWaterReuseOperationData( + request: WaterReuseOperationDataUpdateRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index 093ea859b..a2570a9e2 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -281,6 +281,20 @@ export interface PublicSewageFacilitySubmitResponse { status: string; } +export interface PublicSewageFacilityOperationDataUpdateRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; +} + +export interface PublicSewageFacilityOperationDataUpdateResponse { + data?: string; + message?: string; + status: string; +} + export interface StatisticsInputPublicSewageFacilityService { getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise; getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise; @@ -288,4 +302,7 @@ export interface StatisticsInputPublicSewageFacilityService { saveMainFacilityMapping(param: PublicSewageMainFacilityMappingSaveRequest): Promise; savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise; submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise; + updatePublicSewageFacilityOperationData( + param: PublicSewageFacilityOperationDataUpdateRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts new file mode 100644 index 000000000..b7981456d --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts @@ -0,0 +1,76 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface ExcretaTreatmentFacilityReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + fcltNm: string; + rowType: string; + fcltPstnCn: string | null; + telNo: string | null; + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string | null; + linkTmplntCn: string | null; + 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 | null; + trbtrNm: string | null; + msirbCn: string | null; + ssirbCn: string | null; + operMthdCdCn: string | null; + wholNope: number; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; +} + +export interface ExcretaTreatmentFacilityReviewResponseData { + excretaTreatmentFacilityList: ExcretaTreatmentFacilityReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export interface ExcretaTreatmentFacilityReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; + sewerBzmnCd?: string; +} + +export type ExcretaTreatmentFacilityReviewApiResponse = ApiResponse< + ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData +>; + +export function extractExcretaTreatmentFacilityReviewList( + data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined, +): ExcretaTreatmentFacilityReviewItem[] { + if (!data) return []; + if (Array.isArray(data)) return data; + return data.excretaTreatmentFacilityList ?? []; +} + +export function extractExcretaTreatmentFacilityReviewStatsStatusInfo( + data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} diff --git a/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts new file mode 100644 index 000000000..e75dcf3eb --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts @@ -0,0 +1,152 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface PrivateSewageFacilityReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00080001 검토 `data.privateSewageList[]` 한 행 */ +export interface PrivateSewageTreatFactStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: 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; + rmrk: string | null; +} + +/** ST00080002 검토 `data.septicTankList[]` 한 행 */ +export interface SepticTankReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: 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; + rmrk: string | null; +} + +/** ST00080003 검토 `data.mgmtAreaList[]` 한 행 */ +export interface MgmtAreaReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: string; + rowType: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; +} + +export interface PrivateSewageFacilityReviewResponseData { + privateSewageList?: PrivateSewageTreatFactStatusReviewItem[]; + septicTankList?: SepticTankReviewItem[]; + mgmtAreaList?: MgmtAreaReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type PrivateSewageFacilityReviewApiResponse = ApiResponse< + unknown[] | PrivateSewageFacilityReviewResponseData +>; + +export function extractPrivateSewageFacilityReviewList( + tabKey: string, + data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00080001") { + return data.privateSewageList ?? []; + } + if (tabKey === "ST00080002") { + return data.septicTankList ?? []; + } + if (tabKey === "ST00080003") { + return data.mgmtAreaList ?? []; + } + return []; +} + +export function extractPrivateSewageFacilityReviewStatsStatusInfo( + data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +/** 검토 목록 null → 그리드 표시용 빈 문자열 */ +export function mapPrivateSewageFacilityReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts index 0f34edf86..7ce2bc32a 100644 --- a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts @@ -1,77 +1,82 @@ import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; /** * 05.중계펌프장 검토 아이템 인터페이스 */ export interface MediationPumpStationItem { - // 기본 정보 - ctpvNm: string; - sggNm: string; - admdstCd: string; - statsYr: string; - displayYear: string; - rowType: 'OLD' | 'CURR'; - pumpstNm: string; - impdFcltPstnCn: string; - oprtngStrtYmd: string; - pumpSeNm: string; - swtrmtFcltNm: string; - - // 제원 및 계획량 - planMltvArea: number; - sndyArea: number; - rainArea: number; - exlnQnt: number; - - // 오수펌프 (DRTW) - drtwPumpGrssPpqty: number; - drtwPumpN1DmtrArea: number; - drtwPumpN1Ppqty: number; - drtwPumpN1Cnt: number; - drtwPumpN1RsrvCnt: number; - + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + pumpSeNm: string | null; + swtrmtFcltNm: string | null; + fcltPstnCn: string | null; + oprtngStrtYmd: string | null; + 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; - - // 우수펌프 (EXLN) - exlnPumpGrssPpqty: number; + exlnPumpGrssPpqty: number; exlnPumpN1DmtrArea: number; exlnPumpN1Ppqty: number; exlnPumpN1Cnt: number; - exlnPumpN4RsrvCnt: number; - + exlnPumpN1RsrvCnt: number; exlnPumpN2DmtrArea: number; exlnPumpN2Ppqty: number; exlnPumpN2Cnt: number; exlnPumpN2RsrvCnt: number; - exlnPumpN3DmtrArea: number; exlnPumpN3Ppqty: number; exlnPumpN3Cnt: number; exlnPumpN3RsrvCnt: number; - - // 기타 - anlElpwrUseQnt: number; - rmrk: string; + anlElpwrUseQnt: number; + rmrk: string | null; } - export interface MediationPumpStationResponseData { - pumpStationList: MediationPumpStationItem[]; + pumpingStationList?: MediationPumpStationItem[]; + pumpStationList?: MediationPumpStationItem[]; + statsStatusInfo?: StatsStatusInfo; } +export type MediationPumpStationApiResponse = ApiResponse< + MediationPumpStationItem[] | MediationPumpStationResponseData +>; -export type MediationPumpStationApiResponse = ApiResponse & { - message: string; -}; +export function extractMediationPumpStationList( + data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined, +): MediationPumpStationItem[] { + if (!data) return []; + if (Array.isArray(data)) return data; + const list = data.pumpingStationList ?? data.pumpStationList; + return Array.isArray(list) ? list : []; +} + +export function extractMediationPumpStationStatsStatusInfo( + data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} export interface MediationPumpStationService { getMediationPumpStationList(params: { @@ -81,4 +86,4 @@ export interface MediationPumpStationService { sggNm?: string; pumpstNm?: string; }): Promise; -} \ No newline at end of file +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts new file mode 100644 index 000000000..82d8ebb93 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts @@ -0,0 +1,118 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewageExcretaRelatedReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00120001·ST00120002 검토 `data.staffList[]` 한 행 */ +export interface StaffReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + displayYear: string; + admdstCd: string; + admdstNm: string | null; + rowType: string; + ognzNm: string | null; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; +} + +/** ST00120003 검토 `data.collectionList[]` 한 행 */ +export interface CollectionReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; + rmrk: string | null; +} + +/** ST00120004 검토 `data.businessList[]` 한 행 */ +export interface BusinessReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; + rmrk: string | null; +} + +export interface SewageExcretaRelatedReviewResponseData { + staffList?: StaffReviewItem[]; + collectionList?: CollectionReviewItem[]; + businessList?: BusinessReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewageExcretaRelatedReviewApiResponse = ApiResponse< + unknown[] | SewageExcretaRelatedReviewResponseData +>; + +export function extractSewageExcretaRelatedReviewList( + tabKey: string, + data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00120001" || tabKey === "ST00120002") { + return data.staffList ?? []; + } + if (tabKey === "ST00120003") { + return data.collectionList ?? []; + } + if (tabKey === "ST00120004") { + return data.businessList ?? []; + } + return []; +} + +export function extractSewageExcretaRelatedReviewStatsStatusInfo( + data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewageExcretaRelatedReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + ognzNm: row.ognzNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts new file mode 100644 index 000000000..37b64f502 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts @@ -0,0 +1,229 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewageExcretaSlurgeReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00110001 검토 `data.sludgeProcessingList[]` 한 행 */ +export interface SludgeProcessingReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + transfTmplntNm: string | null; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string | null; + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string | null; + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string | null; + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string | null; + itslfTs1CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string | null; + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string | null; + cparTs2Whol: number; + rmrk: string | null; +} + +/** ST00110002 검토 `data.digestionTankOperList[]` 한 행 */ +export interface DigestionTankOperReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + digstnEnYn: string | null; + digstnCpct: number; + actlDrvnCpct: number; + digstnGasInputQnt: number; + digstnGasInputSldDnst: number; + digstnTnkOprtngDayCnt: number; + ocrnGasQnt: number; + digstnGasUtztnWholPrst: number; + digstnGasUtztnNtslQnt: number; + digstnGasUtztnEtgtQnt: number; + digstnGasUtztnItslfQnt: number; + digstnGasUtztnEtcQnt: number; + surplsGasQnt: number; + rmrk: string | null; +} + +/** ST00110003 검토 `data.sewageSludgeOperList[]` 한 행 */ +export interface SewageSludgeOperReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string | null; + prcsMthExpln: string | null; + fcltCpct: number | null; + anlPrcsQnt: number | null; + dayAvgOprtngHr: number | null; + anlOprtngDayCnt: number | null; + oprtngRt: number | null; + rmrk: string | null; +} + +/** ST00110004 검토 `data.humanWasteSludgeList[]` 한 행 */ +export interface HumanWasteSludgeReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string | null; + gnam: number | null; + wholDspsQnt: number | null; + rcycQnt: number | null; + olandLfvlm: number | null; + incnrQnt: number | null; + oceanInputQnt: number | null; + fuelQnt: number | null; + etcQnt: number | null; + nondspsQnt: number | null; + swtrmtFcltNm: string | null; + rmrk: string | null; +} + +export interface SewageExcretaSlurgeReviewResponseData { + sludgeProcessingList?: SludgeProcessingReviewItem[]; + digestionTankOperList?: DigestionTankOperReviewItem[]; + sewageSludgeOperList?: SewageSludgeOperReviewItem[]; + humanWasteSludgeList?: HumanWasteSludgeReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewageExcretaSlurgeReviewApiResponse = ApiResponse< + unknown[] | SewageExcretaSlurgeReviewResponseData +>; + +export function extractSewageExcretaSlurgeReviewList( + tabKey: string, + data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00110001") { + return data.sludgeProcessingList ?? []; + } + if (tabKey === "ST00110002") { + return data.digestionTankOperList ?? []; + } + if (tabKey === "ST00110003") { + return data.sewageSludgeOperList ?? []; + } + if (tabKey === "ST00110004") { + return data.humanWasteSludgeList ?? []; + } + return []; +} + +export function extractSewageExcretaSlurgeReviewStatsStatusInfo( + data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewageExcretaSlurgeReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + const isHumanWasteSludgeRow = "gnam" in row; + return { + ...row, + admdstNm: row.admdstNm ?? "", + fcltNm: isHumanWasteSludgeRow + ? (row.fcltNm ?? "") + : (row.fcltNm ?? row.swtrmtFcltNm ?? ""), + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + transfTmplntNm: row.transfTmplntNm ?? "", + incnrPrcsFcltNm: row.incnrPrcsFcltNm ?? "", + stbznPrcsFcltNm: row.stbznPrcsFcltNm ?? "", + dryPrcsFcltNm: row.dryPrcsFcltNm ?? "", + fuelPrcsFcltNm: row.fuelPrcsFcltNm ?? "", + cmpsPrcsFcltBzentyNm: row.cmpsPrcsFcltBzentyNm ?? "", + otsdCnsgnFcltNm: row.otsdCnsgnFcltNm ?? "", + // ST00110002 — API 필드 → review 그리드 컬럼 fieldName + digesterYn: row.digesterYn ?? row.digstnEnYn ?? "", + dsgnCpct: row.dsgnCpct ?? row.digstnCpct ?? 0, + actlOperCpct: row.actlOperCpct ?? row.actlDrvnCpct ?? 0, + inputQnt: row.inputQnt ?? row.digstnGasInputQnt ?? 0, + tsConc: row.tsConc ?? row.digstnGasInputSldDnst ?? 0, + oprtngDays: row.oprtngDays ?? row.digstnTnkOprtngDayCnt ?? 0, + gnrtGasQnt: row.gnrtGasQnt ?? row.ocrnGasQnt ?? 0, + gasTotal: row.gasTotal ?? row.digstnGasUtztnWholPrst ?? 0, + gasSale: row.gasSale ?? row.digstnGasUtztnNtslQnt ?? 0, + gasGen: row.gasGen ?? row.digstnGasUtztnEtgtQnt ?? 0, + gasSelf: row.gasSelf ?? row.digstnGasUtztnItslfQnt ?? 0, + gasEtc: row.gasEtc ?? row.digstnGasUtztnEtcQnt ?? 0, + surplusGas: row.surplusGas ?? row.surplsGasQnt ?? 0, + prcsMthExpln: row.prcsMthExpln ?? "", + fcltCpct: row.fcltCpct ?? 0, + anlPrcsQnt: row.anlPrcsQnt ?? 0, + dayAvgOprtngHr: row.dayAvgOprtngHr ?? 0, + anlOprtngDayCnt: row.anlOprtngDayCnt ?? 0, + oprtngRt: row.oprtngRt ?? 0, + // ST00110004 + gnam: row.gnam ?? 0, + wholDspsQnt: row.wholDspsQnt ?? 0, + rcycQnt: row.rcycQnt ?? 0, + olandLfvlm: row.olandLfvlm ?? 0, + incnrQnt: row.incnrQnt ?? 0, + oceanInputQnt: row.oceanInputQnt ?? 0, + fuelQnt: row.fuelQnt ?? 0, + etcQnt: row.etcQnt ?? 0, + nondspsQnt: row.nondspsQnt ?? 0, + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts new file mode 100644 index 000000000..7005dba1a --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts @@ -0,0 +1,174 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewerageFinanceReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00090001 검토 `data.financialSettlementList[]` 한 행 */ +export interface FinancialSettlementReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: 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; + wholSwageImpdFcltAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; +} + +/** ST00090002 검토 `data.sewerageFeeList[]` 한 행 */ +export interface SewerageFeeReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; + rmrk: string | null; +} + +/** ST00090003 검토 `data.usageFeeList[]` 한 행 */ +export interface UsageFeeReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: 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; + rmrk: string | null; +} + +/** ST00090004 검토 `data.orgCauseList[]` 한 행 */ +export interface OrgCauseReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; +} + +export interface SewerageFinanceReviewResponseData { + financialSettlementList?: FinancialSettlementReviewItem[]; + sewerageFeeList?: SewerageFeeReviewItem[]; + usageFeeList?: UsageFeeReviewItem[]; + orgCauseList?: OrgCauseReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewerageFinanceReviewApiResponse = ApiResponse< + unknown[] | SewerageFinanceReviewResponseData +>; + +export function extractSewerageFinanceReviewList( + tabKey: string, + data: unknown[] | SewerageFinanceReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00090001") { + return data.financialSettlementList ?? []; + } + if (tabKey === "ST00090002") { + return data.sewerageFeeList ?? []; + } + if (tabKey === "ST00090003") { + return data.usageFeeList ?? []; + } + if (tabKey === "ST00090004") { + return data.orgCauseList ?? []; + } + return []; +} + +export function extractSewerageFinanceReviewStatsStatusInfo( + data: unknown[] | SewerageFinanceReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewerageFinanceReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts new file mode 100644 index 000000000..2f3d416ce --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts @@ -0,0 +1,101 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewerageServiceLevelReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00130001 검토 `data.complaintList[]` 한 행 */ +export interface ComplaintReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + swtrmtPpltnCnt: number; + whol: number; + odorCnt: number; + swagePplCnt: number; + dschgWqCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + sewerCrgAmt: number; + etcCnt: number; + crtrPpltnCvlcptEvntcsCnt: number; + rmrk: string | null; +} + +/** ST00130002 검토 `data.facilityList[]` 한 행 */ +export interface ResidentFriendlyFacilityReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + fcltCpct: number; + tmplntArea: number; + whol: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; +} + +export interface SewerageServiceLevelReviewResponseData { + complaintList?: ComplaintReviewItem[]; + facilityList?: ResidentFriendlyFacilityReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewerageServiceLevelReviewApiResponse = ApiResponse< + unknown[] | SewerageServiceLevelReviewResponseData +>; + +export function extractSewerageServiceLevelReviewList( + tabKey: string, + data: unknown[] | SewerageServiceLevelReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00130001") { + return data.complaintList ?? []; + } + if (tabKey === "ST00130002") { + return data.facilityList ?? []; + } + return []; +} + +export function extractSewerageServiceLevelReviewStatsStatusInfo( + data: unknown[] | SewerageServiceLevelReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewerageServiceLevelReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + rowType: row.rowType ?? "", + admdstNm: row.admdstNm ?? "", + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts new file mode 100644 index 000000000..d50c3470b --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts @@ -0,0 +1,212 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface WaterReuseReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00100001 검토 `data.reuseStatusList[]` 한 행 */ +export interface ReuseStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string; + sncnYmd: string | null; + prcsFcltCpct: number | string; + prcsQnt: number; + lglYn: string | null; + instlYn: string | null; + oprtngStrtYmd: string | null; + instlAprvYmd: string | null; + rprcsFcltCpct: number; + mtcnsNm: string | null; + 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; + rmrk: string | null; +} + +/** ST00100002 검토 `data.grayWaterList[]` 한 행 */ +export interface GrayWaterReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + fcltPstnCn: string | null; + bldgSeCn: string | null; + lglTrgtYn: string | null; + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string | null; + ruswtrOperAmt: number; + rmrk: string | null; +} + +/** ST00100003 검토 `data.rainwaterList[]` 한 행 */ +export interface RainwaterReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + bdstTypCn: string | null; + fcltPstnCn: string | null; + lglTrgtYn: string | null; + instlCmptnYmd: string | null; + instlAmt: number; + rcptMynCn: string | null; + filtrPrcsFcltYnExpln: string | null; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string | null; + anlOperAmt: number; + rmrk: string | null; +} + +/** ST00100004 검토 `data.regStatusList[]` 한 행 */ +export interface RegStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; + regDt: string | null; + rmrk: string | null; +} + +/** ST00100005 검토 `data.sncnStatusList[]` 한 행 */ +export interface SncnStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; + coRegYmd: string | null; + rmrk: string | null; +} + +export interface WaterReuseReviewResponseData { + reuseStatusList?: ReuseStatusReviewItem[]; + grayWaterList?: GrayWaterReviewItem[]; + rainwaterList?: RainwaterReviewItem[]; + regStatusList?: RegStatusReviewItem[]; + sncnStatusList?: SncnStatusReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type WaterReuseReviewApiResponse = ApiResponse< + unknown[] | WaterReuseReviewResponseData +>; + +export function extractWaterReuseReviewList( + tabKey: string, + data: unknown[] | WaterReuseReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00100001") { + return data.reuseStatusList ?? []; + } + if (tabKey === "ST00100002") { + return data.grayWaterList ?? []; + } + if (tabKey === "ST00100003") { + return data.rainwaterList ?? []; + } + if (tabKey === "ST00100004") { + return data.regStatusList ?? []; + } + if (tabKey === "ST00100005") { + return data.sncnStatusList ?? []; + } + return []; +} + +export function extractWaterReuseReviewStatsStatusInfo( + data: unknown[] | WaterReuseReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapWaterReuseReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + sncnYmd: row.sncnYmd ?? "", + lglYn: row.lglYn ?? "", + instlYn: row.instlYn ?? "", + oprtngStrtYmd: row.oprtngStrtYmd ?? "", + instlAprvYmd: row.instlAprvYmd ?? "", + mtcnsNm: row.mtcnsNm ?? "", + fcltNm: row.fcltNm ?? "", + fcltPstnCn: row.fcltPstnCn ?? "", + bldgSeCn: row.bldgSeCn ?? "", + lglTrgtYn: row.lglTrgtYn ?? "", + cmptnStrtYmd: row.cmptnStrtYmd ?? "", + bdstTypCn: row.bdstTypCn ?? "", + instlCmptnYmd: row.instlCmptnYmd ?? "", + rcptMynCn: row.rcptMynCn ?? "", + filtrPrcsFcltYnExpln: row.filtrPrcsFcltYnExpln ?? "", + exlnUtlzUsgExpln: row.exlnUtlzUsgExpln ?? "", + bplcNm: row.bplcNm ?? "", + rprsvNm: row.rprsvNm ?? "", + bplcPstnCn: row.bplcPstnCn ?? "", + regDt: row.regDt ?? "", + coRegYmd: row.coRegYmd ?? "", + mnUsgCn: row.mnUsgCn ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 60db9d6be..6ba58fb05 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -73,9 +73,11 @@ const StatisticsPage = () => { const DynamicComponent = dynamic( () => - import( - `@/components/statistics/${category}/${resolvedComponentName}` - ), + category + ? import( + `@/components/statistics/${category}/${resolvedComponentName}` + ) + : import(`@/components/statistics/${resolvedComponentName}`), { ssr: false, loading: () => }, ); diff --git a/apps/st/src/shared/lib/statistics-menu-url.ts b/apps/st/src/shared/lib/statistics-menu-url.ts index fc9e74356..ed7e8913c 100644 --- a/apps/st/src/shared/lib/statistics-menu-url.ts +++ b/apps/st/src/shared/lib/statistics-menu-url.ts @@ -55,7 +55,7 @@ export function resolveStatisticsTabId(urlPath: string): string { return componentName || rawUrlPath; } -/** `@/components/statistics/{category}/{Component}` 경로 */ +/** `@/components/statistics/{category}/{Component}` 또는 루트 컴포넌트 경로 */ export function buildStatisticsComponentImportPath( urlPath: string, parentEngNm: string, @@ -63,6 +63,9 @@ export function buildStatisticsComponentImportPath( const { componentName } = parseStatisticsMenuUrlPath(urlPath); const category = resolveStatisticsCategory(urlPath, parentEngNm); const name = componentName || urlPath; + if (!category) { + return `@/components/statistics/${name}`; + } return `@/components/statistics/${category}/${name}`; } diff --git a/apps/st/src/shared/ui/tab-bar.tsx b/apps/st/src/shared/ui/tab-bar.tsx index 392e1b199..d6fd4db9a 100644 --- a/apps/st/src/shared/ui/tab-bar.tsx +++ b/apps/st/src/shared/ui/tab-bar.tsx @@ -1,10 +1,57 @@ "use client"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + import { useTabStore } from "@/store/tab-store"; import { Tabs, TabsList, TabsTrigger } from "@common/components"; +type TabContextMenuState = { + x: number; + y: number; + tabId: string; +}; + const TabBar = () => { - const { tabs, activeTabId, setActiveTab, removeTab } = useTabStore(); + const { + tabs, + activeTabId, + setActiveTab, + removeTab, + closeAllTabs, + closeOtherTabs, + closeTabsToLeft, + closeTabsToRight, + } = useTabStore(); + const [contextMenu, setContextMenu] = useState(null); + const contextMenuRef = useRef(null); + + const closeContextMenu = useCallback(() => { + setContextMenu(null); + }, []); + + useEffect(() => { + if (!contextMenu) return; + + const onPointerDown = (event: PointerEvent) => { + if (contextMenuRef.current?.contains(event.target as Node)) return; + closeContextMenu(); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") closeContextMenu(); + }; + const onScroll = () => closeContextMenu(); + + window.addEventListener("pointerdown", onPointerDown); + window.addEventListener("keydown", onKeyDown); + window.addEventListener("scroll", onScroll, true); + + return () => { + window.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("scroll", onScroll, true); + }; + }, [closeContextMenu, contextMenu]); if (tabs.length === 0) { return null; @@ -18,36 +65,128 @@ const TabBar = () => { removeTab(tabId); }; + const handleTabContextMenu = ( + e: React.MouseEvent, + tabId: string, + ) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ x: e.clientX, y: e.clientY, tabId }); + }; + + const handleCloseOtherTabs = () => { + if (!contextMenu || tabs.length <= 1) return; + closeOtherTabs(contextMenu.tabId); + closeContextMenu(); + }; + + const handleCloseAllTabs = () => { + closeAllTabs(); + closeContextMenu(); + }; + + const contextTabIndex = contextMenu + ? tabs.findIndex((tab) => tab.id === contextMenu.tabId) + : -1; + const canCloseTabsToLeft = contextTabIndex > 0; + const canCloseTabsToRight = + contextTabIndex >= 0 && contextTabIndex < tabs.length - 1; + const canCloseOtherTabs = tabs.length > 1; + + const handleCloseTabsToLeft = () => { + if (!contextMenu || !canCloseTabsToLeft) return; + closeTabsToLeft(contextMenu.tabId); + closeContextMenu(); + }; + + const handleCloseTabsToRight = () => { + if (!contextMenu || !canCloseTabsToRight) return; + closeTabsToRight(contextMenu.tabId); + closeContextMenu(); + }; + return ( - - - {tabs.map((tab) => ( - - - {tab.label} - - handleCloseTab(e, tab.id)} - aria-label={`${tab.label} 탭 닫기`} - className="flex flex-shrink-0 items-center justify-center leading-none hover:text-[#2094fa]" + <> + + + {tabs.map((tab) => ( + handleTabContextMenu(e, tab.id)} > - - - - ))} - - + + {tab.label} + + handleCloseTab(e, tab.id)} + aria-label={`${tab.label} 탭 닫기`} + className="flex flex-shrink-0 items-center justify-center leading-none hover:text-[#2094fa]" + > + + + + ))} + + + + {contextMenu && + typeof document !== "undefined" && + createPortal( +
e.preventDefault()} + > + + + + +
, + document.body, + )} + ); }; diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 3a75f37aa..7e942879a 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -89,6 +89,8 @@ interface TabStore { setMenuList: (menuList: MenuItem[] | null) => void; closeAllTabs: () => void; closeOtherTabs: (tabId: string) => void; + closeTabsToLeft: (tabId: string) => void; + closeTabsToRight: (tabId: string) => void; } export const TAB_STORE_PERSIST_KEY = "st-tab-store"; @@ -240,6 +242,46 @@ const useTabStoreHook = create()( }); } }, + closeTabsToLeft: (tabId: string) => { + const { tabs, activeTabId } = get(); + const index = tabs.findIndex((tab) => tab.id === tabId); + if (index <= 0) return; + + const newTabs = tabs.slice(index); + let newActiveTabId = activeTabId; + if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) { + newActiveTabId = tabId; + } + + set({ + tabs: newTabs, + activeTabId: newActiveTabId, + }); + if (newActiveTabId) { + const nextActive = get().tabs.find((t) => t.id === newActiveTabId); + if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive); + } + }, + closeTabsToRight: (tabId: string) => { + const { tabs, activeTabId } = get(); + const index = tabs.findIndex((tab) => tab.id === tabId); + if (index === -1 || index >= tabs.length - 1) return; + + const newTabs = tabs.slice(0, index + 1); + let newActiveTabId = activeTabId; + if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) { + newActiveTabId = tabId; + } + + set({ + tabs: newTabs, + activeTabId: newActiveTabId, + }); + if (newActiveTabId) { + const nextActive = get().tabs.find((t) => t.id === newActiveTabId); + if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive); + } + }, }), { name: TAB_STORE_PERSIST_KEY,