From 60c515a65428624acc1cec084350d957a1cdeba0 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 12 Feb 2026 10:50:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?st:=20fix=20&=20refactor:=20=ED=95=98?= =?UTF-8?q?=EC=88=98=EB=8F=84=20=EB=B3=B4=EA=B8=89=EB=A5=A0=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=A1=B0=ED=9A=8C=20response=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20TadaGrid=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20Tab=20state=20=EB=B6=80=EB=AA=A8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20prop=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20grid=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EC=9C=84=ED=95=9C=20ref?= =?UTF-8?q?=20prop=20=EC=88=98=EC=A0=95(TabDataGrid.tsx=EB=8A=94=20?= =?UTF-8?q?=EC=95=84=EC=A7=81=20=EB=AF=B8=EA=B5=AC=ED=98=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/DataGrid.tsx | 39 +++- apps/st/src/components/module/TabDataGrid.tsx | 18 +- .../input/SeweragePenetrationRateInp.tsx | 133 ++++++++++- .../statistics/review/SewagePipeRvw.tsx | 51 +++-- .../review/SeweragePenetrationRateRvw.tsx | 2 +- .../statistics/review/StatsInputStatusRvw.tsx | 2 +- .../ManagementVerificationRule.tsx | 3 +- apps/st/src/components/type/DataGrid.type.ts | 6 + .../input/SeweragePenetrationRateInpColumn.ts | 18 +- .../util/column/review/SewagePipeRvwColumn.ts | 216 +++++++++--------- .../api/statistics-input-service.ts | 11 + .../api/statistics-management-service.ts | 2 +- .../model/statistics-input-interface.ts | 71 +++++- 13 files changed, 413 insertions(+), 159 deletions(-) diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index b762ed5cb..4e423571d 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -164,7 +164,7 @@ function createOnSortingHandler( const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName); const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0]; const groupField = mergeCellCol?.fieldName || null; - const orderByField = 'rgnNm'; // 고정값 또는 설정 가능하게 할 수 있음 + const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) { @@ -289,7 +289,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT }); } -const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport = true, fixedColumns, showHeaderCheckbox = false, rowData, setRowData }: DataGridProps) => { +const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport = true, fixedColumns, showHeaderCheckbox = false, rowData, setRowData, getGridDataRef }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); const fileInputRef = useRef(null); @@ -748,8 +748,28 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport }); }, 200); - }, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox]); + if (getGridDataRef) { + const fieldNames = columns.map((c) => c.fieldName).filter(Boolean) as string[]; + getGridDataRef.current = () => { + const gv = gridRef.current?.gridView as { getDataSource?: () => { getRowCount(): number; getValue(r: number, f: string): unknown } } | undefined; + if (!gv?.getDataSource) return []; + const ds = gv.getDataSource(); + if (!ds) return []; + const cnt = ds.getRowCount(); + const rows: Record[] = []; + for (let i = 0; i < cnt; i++) { + const r: Record = {}; + for (const f of fieldNames) r[f] = ds.getValue(i, f) as string | number; + rows.push(r); + } + return rows; + }; + } + return () => { + if (getGridDataRef) getGridDataRef.current = null; + }; + }, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox, getGridDataRef]); return (
@@ -782,13 +802,14 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport rows={rowData} onInitialized={(grid) => { (grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef); - const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record[]) => void } | undefined; - if (dp?.setFields) { - dp.setFields(buildFieldsFromColumns(columns)); + const dataProvider = gridRef.current?.dataProvider as unknown as + | { setFields?(f: unknown[]): void; setRows?(r: Record[]): void; getJsonRow?(r: number): Record } + | undefined; + if (dataProvider?.setFields) { + dataProvider.setFields(buildFieldsFromColumns(columns)); } - // setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지) - if (dp?.setRows && rowData?.length) { - dp.setRows(rowData); + if (dataProvider?.setRows && rowData?.length) { + dataProvider.setRows(rowData); } }} > diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 532a342bd..a7e0e3383 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -162,7 +162,7 @@ function createOnSortingHandler( const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName); const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0]; const groupField = mergeCellCol?.fieldName || null; - const orderByField = 'rgnNm'; // 고정값 또는 설정 가능하게 할 수 있음 + const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) { @@ -306,7 +306,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string }; } -const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap }: TabDataGridProps) => { +const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange }: TabDataGridProps) => { // 각 탭별로 그리드 ref와 데이터를 관리 const gridRefs = useRef>(new Map()); const fileInputRefs = useRef>(new Map()); @@ -316,11 +316,14 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r const { setMessage: alert } = useAlertStore((state) => state); const activeTabId = useTabStore((state) => state.activeTabId); const menuList = useTabStore((state) => state.menuList); - const [activeTab, setActiveTab] = useState(() => { - // 초기 활성 탭을 첫 번째 tabKey로 설정 + + const [internalActiveTab, setInternalActiveTab] = useState(() => { const firstTab = columnInfo[0]; return firstTab?.tabKey || ''; }); + const isControlled = activeTabProp !== undefined; + const activeTab = isControlled ? activeTabProp : internalActiveTab; + const setActiveTab = isControlled ? (onActiveTabChange ?? (() => {})) : setInternalActiveTab; const exportExcel = (tabKey: string) => { const currentRowData = rowDataMap.get(tabKey) || []; @@ -803,10 +806,15 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r const columns = currentInfo.columns; - const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined; + const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record[]) => void } | undefined; if (dp?.setFields) { dp.setFields(buildFieldsFromColumns(columns)); } + // setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지) + const tabRowData = rowDataMap.get(tabKey) || []; + if (dp?.setRows && tabRowData.length) { + dp.setRows(tabRowData); + } gridView.setDisplayOptions({ fitStyle: GridFitStyle.EVEN diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 7644ab1e3..70d4f10ae 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useRef } from "react"; import SearchForm from "@/components/module/SearchForm"; // import { DataGrid } from "@rsone/components"; @@ -8,6 +8,7 @@ import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import { LoadingWithAnimation } from "@common/components"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { useAlertStore } from "@/shared/lib"; import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service"; @@ -15,9 +16,13 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SewerageSupplyRateItem, SewerageSupplyRateRequestParams, + SewerageSupplyRateSaveItem, } from "@/entities/statistics/model/statistics-input-interface"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { getFooterSummary } from "@/components/util/grid/footerSummary"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; function getSewerageSupplyRateParams( values: SearchFormValues, @@ -28,7 +33,6 @@ function getSewerageSupplyRateParams( gugun: values.gugun === 'ALL' ? '' : values.gugun, bsnmCd: values.bsnmCd, yr: overrides?.yr ?? values.year, - demoYr: overrides?.demoYr ?? values.year, ...(overrides?.mode && { mode: overrides.mode }), }; } @@ -41,22 +45,38 @@ const SeweragePenetrationRateInp = () => { const [searchValues, setSearchValues] = useState(null); const [isEnterable, setIsEnterable] = useState(true); const [lastSearchedYear, setLastSearchedYear] = useState(null); + const { confirm } = useConfirm(); + const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number } | null>(null); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + + const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm'] as const; + const footerWithApiDefault = (fieldName: (typeof footerFieldNames)[number], numberFormat: string) => ({ + expression: 'sum' as const, + numberFormat, + valueCallback: (grid: Parameters[0], column: Parameters[1]) => + (footerFromApi && footerFromApi[fieldName] != null ? footerFromApi[fieldName] : null) ?? getFooterSummary(grid, column), + }); // 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요. const columnInfo = useMemo(() => { const { columns, columnLayout } = SeweragePenetrationRateInpColumn; + const base = isEnterable ? columns : columns.map((col) => ({ ...col, editable: false as const })); return { columnLayout, - columns: isEnterable - ? columns - : columns.map((col) => ({ ...col, editable: false as const })), + columns: base.map((col) => { + if (col.fieldName === 'totalPopl') return { ...col, footer: footerWithApiDefault('totalPopl', '#,##0') }; + if (col.fieldName === 'poplSm') return { ...col, footer: footerWithApiDefault('poplSm', '#,##0') }; + if (col.fieldName === 'frgnrPoplSm') return { ...col, footer: footerWithApiDefault('frgnrPoplSm', '#,##0') }; + return col; + }), }; - }, [isEnterable]); + }, [isEnterable, footerFromApi]); const handleReset = () => { setRowData([]); setLastSearchedYear(null); + setFooterFromApi(null); }; const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => { @@ -64,18 +84,30 @@ const SeweragePenetrationRateInp = () => { setIsLoading(true); const response = await statisticsInputAPi.getSewerageSupplyRateList(params); if (response.status === 'success' && response.data) { + const commonInfo = response.data.commonInfo; const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({ ...d, + sido: commonInfo.sido, speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr, })); - setRowData(toGridRows(convertedData)); + + setFooterFromApi( + commonInfo + ? { + totalPopl: commonInfo.bsnmTotalPopl, + poplSm: commonInfo.bsnmPoplSm, + frgnrPoplSm: commonInfo.bsnmFrgnrPoplSm, + } + : null + ); } else { alert(response.message); } } catch { alert('데이터 조회 중 오류가 발생했습니다.'); setRowData([]); + setFooterFromApi(null); } finally { setIsLoading(false); } @@ -126,11 +158,91 @@ const SeweragePenetrationRateInp = () => { // 제출 로직 }; - const saveFunc = () => { - // 저장 로직 + const saveFunc = async () => { + try { + if (await confirm('저장 하시겠습니까?')) { + setIsLoading(true); + + const userInfo = await getUserInfo(); + const currentData = getGridDataRef.current?.(); + + if (!currentData || currentData.length === 0) { + alert('저장할 데이터가 없습니다.'); + return; + } + + // 컬럼 정의에서 dataFormat 추출 (소수점 자리수 맞추기 위해) + const columnFormatMap = new Map(); + SeweragePenetrationRateInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + // 숫자 포맷팅 함수 (dataFormat에 따라 소수점 자리수 맞춤) + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === '') return 0; + const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value; + if (isNaN(num)) return 0; + + const format = columnFormatMap.get(fieldName); + if (!format) return num; + + // #,##0.000 → 소수점 3자리, #,##0.0 → 소수점 1자리, #,##0 → 정수 + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + + // SaveItem 타입에 정의된 필드만 추출 + const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ + bsnmCd: searchValues?.bsnmCd ?? '', + sido: d.sido as string, + gugun: (d.gugun as string) || null, + yr: searchValues?.year ?? '', + adstCd: d.adstCd as string, + adstDong: d.adstDong as string, + bstrm: d.bstrm as string, + speclCntrplnAr: d.speclCntrplnAr === '해당없음' ? null : (d.speclCntrplnAr as string | null), + pblswrgPrmTrt: formatNumber(d.pblswrgPrmTrt, 'pblswrgPrmTrt'), + pblswrgScdTrt: formatNumber(d.pblswrgScdTrt, 'pblswrgScdTrt'), + pblswrgTertTrt: formatNumber(d.pblswrgTertTrt, 'pblswrgTertTrt'), + wstrPrmTrt: formatNumber(d.wstrPrmTrt, 'wstrPrmTrt'), + wstrScdTrt: formatNumber(d.wstrScdTrt, 'wstrScdTrt'), + wstrTertTrt: formatNumber(d.wstrTertTrt, 'wstrTertTrt'), + notConectPopltn: formatNumber(d.notConectPopltn, 'notConectPopltn'), + swgeProcZoneInnerAr: formatNumber(d.swgeProcZoneInnerAr, 'swgeProcZoneInnerAr'), + townUnprocPopltn: formatNumber(d.townUnprocPopltn, 'townUnprocPopltn'), + townSwttPopltn: formatNumber(d.townSwttPopltn, 'townSwttPopltn'), + townWrrtnPopltn: formatNumber(d.townWrrtnPopltn, 'townWrrtnPopltn'), + swgeProcZoneOuterAr: formatNumber(d.swgeProcZoneOuterAr, 'swgeProcZoneOuterAr'), + rmk: (d.rmk as string) ?? '', + popltnSm: formatNumber(d.popltnSm, 'popltnSm'), + arSm: formatNumber(d.arSm, 'arSm'), + swgeProcZoneInnerSm: formatNumber(d.swgeProcZoneInnerSm, 'swgeProcZoneInnerSm'), + pblswrgPopltnSm: formatNumber(d.pblswrgPopltnSm, 'pblswrgPopltnSm'), + wstrPopltnSm: formatNumber(d.wstrPopltnSm, 'wstrPopltnSm'), + swgeProcZoneOuterSm: formatNumber(d.swgeProcZoneOuterSm, 'swgeProcZoneOuterSm'), + pblswrgPopltnSuprt: formatNumber(d.pblswrgPopltnSuprt, 'pblswrgPopltnSuprt'), + adtrtPopltnSuprt: formatNumber(d.adtrtPopltnSuprt, 'adtrtPopltnSuprt'), + swrgInstlrt: formatNumber(d.swrgInstlrt, 'swrgInstlrt'), + userId: userInfo.userName, + })); + + const response = await statisticsInputAPi.saveSewerageSupplyRate(convertedData); + console.log(convertedData) + console.log(response); + } else { + // 저장 취소 + } + } catch { + alert('데이터 저장 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } }; return ( + <>
{rowData.length > 0 && @@ -159,8 +271,11 @@ const SeweragePenetrationRateInp = () => { fixedColumns={1} rowData={rowData} setRowData={setRowData} + getGridDataRef={getGridDataRef} />
+ + ); }; diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index ccfc62355..484c0d574 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, act } from "react"; import TabDataGrid from "@/components/module/TabDataGrid"; import SearchForm from "@/components/module/SearchForm"; import SaveButton from "@/components/button/SaveButton"; @@ -18,6 +18,7 @@ const SewagePipeRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState>(new Map()); const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState('a'); const columnInfo = useMemo(() => { return SewagePipeColumnInfoListRvw.map((tab: any) => ({ @@ -40,23 +41,29 @@ const SewagePipeRvw = () => { try { setIsLoading(true); - const response = await statisticsManagementApi.getSewagePipeReviewList({ + + const searchParam = { sido: values.sido, gugun: values.gugun === 'ALL' ? '' : values.gugun, bsnmCd: values.bsnmCd, yr: values.year, - }); + }; - if (response.status === 'success' && response.data) { + let response; + + if (activeTab === 'a') { + response = await statisticsManagementApi.getSewagePipeReviewList(searchParam); + } + + if (response?.status === 'success' && response.data) { const rawList = response.data; const newRowDataMap = new Map(); - - newRowDataMap.set('a', toGridRows(rawList)); - + + newRowDataMap.set(activeTab, toGridRows(rawList)); + setRowDataMap(newRowDataMap); } else { - handleReset(); - alert(response.message || "데이터가 없습니다."); + alert(response?.message || "데이터가 없습니다."); } } catch (error) { console.error("조회 에러:", error); @@ -68,30 +75,32 @@ const SewagePipeRvw = () => { }; return ( -
+
- +
- {}} /> - {}} /> - {}} /> - {}} /> + { }} /> + { }} /> + { }} /> + { }} />
{isLoading && ( -
+
)} -
); diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index bf6ef9911..8ad001c4c 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -93,7 +93,7 @@ const SeweragePenetrationRateRvw = () => {
{isLoading && ( -
+
diff --git a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx index 7a7bb4788..2c11f7abc 100644 --- a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx +++ b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx @@ -91,7 +91,7 @@ const StatsInputStatusRvw = () => {
{isLoading && ( -
+
diff --git a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx index 1d5dfd169..708e2c02e 100644 --- a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx +++ b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx @@ -1,7 +1,8 @@ "use client"; import { useState } from "react"; -import { DataGrid } from "@rsone/components"; +// import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import { Button, diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 4d31facbe..8079148d6 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -150,6 +150,8 @@ export type DataGridProps = { showHeaderCheckbox?: boolean; // 헤더 체크박스 표시 여부 (기본값: true) rowData: Record[]; setRowData: React.Dispatch[]>>; + /** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */ + getGridDataRef?: { current: (() => Record[]) | null }; }; export type TabDataGridProps = { @@ -162,6 +164,10 @@ export type TabDataGridProps = { rowDataMap: Map[]>; setRowDataMap: React.Dispatch[]>>>; isSplitGrid?: boolean; // 5:5 분리형 그리드 유무 + /** 활성 탭 키 (제어 모드). 부모에서 넘기면 해당 탭이 선택됨 */ + activeTab?: string; + /** 활성 탭 변경 시 호출 (제어 모드). activeTab과 함께 넘기면 부모가 탭 상태를 가짐 */ + onActiveTabChange?: (tabKey: string) => void; }; export type SplitDataGridProps = { diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index 39d3c2234..159726976 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -1,4 +1,4 @@ -import { footerInt, footerDecimal1, footerDecimal3, footerAvgDecimal } from "@/components/util/grid/footerSummary"; +import { footerInt, footerDecimal3, footerAvgDecimal } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; /** * 하수도 통계 관리 > 01. 하수도 보급률 @@ -32,7 +32,17 @@ export const SeweragePenetrationRateInpColumn = { { name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 총인구 - { name: 'popltnSm', fieldName: 'popltnSm', header: { text: '총인구(명)' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'popltnSm', + fieldName: 'popltnSm', + header: { text: '총인구(명)' }, + footer: footerInt, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const }, + styleName: 'rg-cell-total', + }, // 총면적 { @@ -55,7 +65,7 @@ export const SeweragePenetrationRateInpColumn = { editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const }, + computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], type: 'sum' as const }, styleName: 'rg-cell-total', }, @@ -170,6 +180,7 @@ export const SeweragePenetrationRateInpColumn = { { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, { name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로보급률(%)' }, visible: false }, + { name: 'adstCd', fieldName: 'adstCd', header: { text: '행정코드' }, visible: false }, ], columnLayout: [ @@ -240,5 +251,6 @@ export const SeweragePenetrationRateInpColumn = { 'swrgInstlrt', 'rmk', 'piplSupprt', + 'adstCd', ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 732752343..28c2b4ad1 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -18,142 +18,144 @@ const rawList = [ mergeCells: true, editable: false, sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { - uniqueValues.add(String(divisionValue)); + footer: [ + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; + return '합계'; } - const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; - } - }, - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { - uniqueValues.add(String(divisionValue)); + }, + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; + return '합계'; } - const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; } - } - ] - }, + ] + }, { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - editable: false, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { - uniqueValues.add(String(divisionValue)); + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { - uniqueValues.add(String(divisionValue)); + }, + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; } - } - ] - }, + ] + }, /* 하수정비 기본계획 */ { name: 'targetYr', fieldName: 'targetYr', header: { text: '하수정보기본계획목표년도' } }, { name: 'piplImprmnPlanExtn', fieldName: 'piplImprmnPlanExtn', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'piplPlanExtn', fieldName: 'piplPlanExtn', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'piplPlanExtn', fieldName: 'piplPlanExtn', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, /* 합류식(m) */ - { name: 'cmbnPlanAr', fieldName: 'cmbnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'cmbnImprmnPlanExtn', fieldName: 'cmbnImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnPlanExtn', fieldName: 'cmbnPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnFcltyExtn', fieldName: 'cmbnFcltyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnRctglExtn', fieldName: 'cmbnRctglExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnCicleExtn', fieldName: 'cmbnCicleExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnOpswrExtn', fieldName: 'cmbnOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnSdgttrExtn', fieldName: 'cmbnSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnPlanAr', fieldName: 'cmbnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cmbnImprmnPlanExtn', fieldName: 'cmbnImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnPlanExtn', fieldName: 'cmbnPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnFcltyExtn', fieldName: 'cmbnFcltyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnRctglExtn', fieldName: 'cmbnRctglExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnCicleExtn', fieldName: 'cmbnCicleExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnOpswrExtn', fieldName: 'cmbnOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnSdgttrExtn', fieldName: 'cmbnSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, /* 분류식 - 오수관로 */ - { name: 'pltwtrPlanAr', fieldName: 'pltwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'pltwtrImprmnPlanExtn', fieldName: 'pltwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrPlanExtn', fieldName: 'pltwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrFlctyExtn', fieldName: 'pltwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrSqreExtn', fieldName: 'pltwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrCiclExtn', fieldName: 'pltwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrPlanAr', fieldName: 'pltwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'pltwtrImprmnPlanExtn', fieldName: 'pltwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrPlanExtn', fieldName: 'pltwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrFlctyExtn', fieldName: 'pltwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrSqreExtn', fieldName: 'pltwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrCiclExtn', fieldName: 'pltwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, /* 분류식 - 우수관로 */ - { name: 'rainwtrPlanAr', fieldName: 'rainwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'rainwtrImprmnPlanExtn', fieldName: 'rainwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrPlanExtn', fieldName: 'rainwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrFlctyExtn', fieldName: 'rainwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrSqreExtn', fieldName: 'rainwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrCiclExtn', fieldName: 'rainwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrOpswrExtn', fieldName: 'rainwtrOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrSdgttrExtn', fieldName: 'rainwtrSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrPlanAr', fieldName: 'rainwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'rainwtrImprmnPlanExtn', fieldName: 'rainwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrPlanExtn', fieldName: 'rainwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrFlctyExtn', fieldName: 'rainwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrSqreExtn', fieldName: 'rainwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrCiclExtn', fieldName: 'rainwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrOpswrExtn', fieldName: 'rainwtrOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrSdgttrExtn', fieldName: 'rainwtrSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, /* 부속시설 */ - { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrMnhleSite', fieldName: 'pltwtrMnhleSite', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrMnhleSite', fieldName: 'rainwtrMnhleSite', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrRcvrSite', fieldName: 'rainwtrRcvrSite', header: { text: '빗물받이(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'soilspllSite', fieldName: 'soilspllSite', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'soilholeSite', fieldName: 'soilholeSite', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrMnhleSite', fieldName: 'pltwtrMnhleSite', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrMnhleSite', fieldName: 'rainwtrMnhleSite', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrRcvrSite', fieldName: 'rainwtrRcvrSite', header: { text: '빗물받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'soilspllSite', fieldName: 'soilspllSite', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'soilholeSite', fieldName: 'soilholeSite', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, /* 대장 및 기타 */ - { name: 'sewerRegstrCmptExtn', fieldName: 'sewerRegstrCmptExtn', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sewerRegstrCmptRt', fieldName: 'sewerRegstrCmptRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision,valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sewerRegstrCmptExtn', fieldName: 'sewerRegstrCmptExtn', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sewerRegstrCmptRt', fieldName: 'sewerRegstrCmptRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, ], columnLayout: [ - { name: 'areaGroup', header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'gugun', - 'adstDong', - ] }, + { + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'adstDong', + ] + }, 'displayYear', 'targetYr', 'piplImprmnPlanExtn', 'piplPlanExtn', 'piplFcltyExtn', 'piplSupprt', { diff --git a/apps/st/src/entities/statistics/api/statistics-input-service.ts b/apps/st/src/entities/statistics/api/statistics-input-service.ts index 32cae5723..bf758e0a8 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-service.ts @@ -6,6 +6,8 @@ import { type SewerageSupplyRateRequestParams, type SewerageSupplyRateApiResponse, type CheckInputPeriodResponse, + type SewerageSupplyRateSaveItem, + type SewerageSupplyRateSaveApiResponse, } from "../model/statistics-input-interface"; export class StatisticsInputAPiService implements StatisticsInputService { @@ -27,6 +29,15 @@ export class StatisticsInputAPiService implements StatisticsInputService { return response.data; } + + public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveItem[]): Promise { + const response = await axios.post ( + `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, + params + ); + + return response.data; + } } export const statisticsInputAPi = new StatisticsInputAPiService(); \ No newline at end of file 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 b566d19c3..a3d46b68b 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -67,7 +67,7 @@ export class StatisticsManagementApiService { } ): Promise { const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/pipe/tab1`, + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/regional-status`, { params } ); return response.data; diff --git a/apps/st/src/entities/statistics/model/statistics-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-interface.ts index 6a76d567e..4f36f49b3 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-interface.ts @@ -13,6 +13,28 @@ export interface SewerageSupplyRateStatusInfo { returnOpinionSm: string | null; } +export interface SewerageSupplyRateCommonInfo { + bsnmArea: number; + bsnmCd: string; + bsnmFrgnrPoplSm: number; + bsnmPoplSm: number; + bsnmTotalPopl: number; + piplFcltyExtn: number; + piplPlanExtn: number; + piplSupprt: number; + sido: string; + yr: string; +} + +/** 검증 결과 한 건 (필드별 메시지) */ +export interface VerificationResultItem { + type: string; + rsn: string; +} + +/** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */ +export type SewerageSupplyRateVerificationResult = Record; + export interface SewerageSupplyRateItem { sido: string; gugun: string; @@ -93,11 +115,13 @@ export interface SewerageSupplyRateItem { chkAdtrtPopltnSuprt: string; chkSwrgInstlrt: string; chkNotConectPopltn: string; + verificationResult?: SewerageSupplyRateVerificationResult; } export interface SewerageSupplyRateResponseData { statusInfo: SewerageSupplyRateStatusInfo; sewerageSupplyRateList: SewerageSupplyRateItem[]; + commonInfo: SewerageSupplyRateCommonInfo; } export type SewerageSupplyRateApiResponse = ApiResponse & { @@ -109,10 +133,53 @@ export interface SewerageSupplyRateRequestParams { gugun?: string; bsnmCd?: string; yr?: string; - demoYr?: string; mode?: string; } +/** 하수도 보급률 저장 API 요청 body (한 행) */ +export interface SewerageSupplyRateSaveItem { + bsnmCd: string; + sido: string; + gugun: string | null; + yr: string; + adstCd: string; + adstDong: string; + bstrm: string; + speclCntrplnAr: string | null; + pblswrgPrmTrt: number; + pblswrgScdTrt: number; + pblswrgTertTrt: number; + wstrPrmTrt: number; + wstrScdTrt: number; + wstrTertTrt: number; + notConectPopltn: number; + swgeProcZoneInnerAr: number; + townUnprocPopltn: number; + townSwttPopltn: number; + townWrrtnPopltn: number; + swgeProcZoneOuterAr: number; + rmk: string; + popltnSm: number; + arSm: number; + swgeProcZoneInnerSm: number; + pblswrgPopltnSm: number; + wstrPopltnSm: number; + swgeProcZoneOuterSm: number; + pblswrgPopltnSuprt: number; + adtrtPopltnSuprt: number; + swrgInstlrt: number; + userId: string; +} + +export interface SewerageSupplyRateSaveData { + hasMismatch?: boolean; + mismatchIdList?: string[]; + httpStatus?: number; + code?: string; +} + +export type SewerageSupplyRateSaveApiResponse = ApiResponse; + // 하수도 통계 입력 기간 조회 export interface CheckInputPeriodResponse { status: string; @@ -122,4 +189,6 @@ export interface CheckInputPeriodResponse { export interface StatisticsInputService { getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise; + checkInputPeriod(params: string): Promise; + saveSewerageSupplyRate(params: SewerageSupplyRateSaveItem[]): Promise; } \ No newline at end of file From efbef8283b97e49874f308dceb1ff3ff18e0f4cc Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 12 Feb 2026 11:10:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=2002.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20tab2=20=EC=8B=9C=EC=84=A4=EB=B3=84=20=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=ED=98=84=ED=99=A9=20=EC=BB=AC=EB=9F=BC=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/SewagePipeRvw.tsx | 2 +- .../util/column/review/SewagePipeRvwColumn.ts | 235 ++++++++++++------ .../api/statistics-management-service.ts | 31 ++- ...statistics-sewage-pipe-review-interface.ts | 79 +++++- 4 files changed, 265 insertions(+), 82 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index ccfc62355..f2416892e 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -40,7 +40,7 @@ const SewagePipeRvw = () => { try { setIsLoading(true); - const response = await statisticsManagementApi.getSewagePipeReviewList({ + const response = await statisticsManagementApi.getSewagePipeReviewRegionalList({ sido: values.sido, gugun: values.gugun === 'ALL' ? '' : values.gugun, bsnmCd: values.bsnmCd, diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 732752343..baec9bf0d 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -184,78 +184,171 @@ const rawList = [ 'pltwtrRcvrSite', 'rainwtrRcvrSite', 'soilspllSite', 'soilholeSite', 'sewerRegstrCmptExtn', 'sewerRegstrCmptRt', 'rmk' ] }, - { - tabKey: 'b', - tabLabel: '시설별 설치현황', - rowStartNum: 5, - fixedColumns: 4, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: '구분', fieldName: '구분', header: { text: '구분' } }, - { name: '하수처리장명', fieldName: '하수처리장명', header: { text: '하수처리장명' } }, - { name: 'FCT_CPC', fieldName: '하수처리시설용량(㎥/일)', header: { text: '하수처리\n시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - { name: 'PROC_ZONE_SM', fieldName: '합계(면적)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'CMBN_PROC_ZONE_AR', fieldName: '합류식(면적)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'SPRT_PROC_ZONE_AR', fieldName: '분류식(면적)', header: { text: '분류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - - { name: 'SEP_RATE', fieldName: '분류식화', header: { text: '분류식화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - { name: 'DRNG_EQP_SM', fieldName: '합계(배수)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CMBN_DRNG_EQP', fieldName: '합류식(배수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPRT_DRNG_EQP_SM', fieldName: '합계(분류배수)', header: { text: '합계(분류)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPRT_DRNG_EQP_CNNC', fieldName: '연결', header: { text: '연결' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPRT_DRNG_EQP_DCNNC', fieldName: '미연결', header: { text: '미연결' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - { name: 'PIPL_FCLTY_EXTN', fieldName: '총 시설연장(m)', header: { text: '총\n시설연장\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - { name: 'ICG_MNN_SM', fieldName: '합계(차집)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'ICG_EXTN', fieldName: '차집', header: { text: '차집' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'MNN_EXTN', fieldName: '오수간선', header: { text: '오수간선' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - { name: '합류식', fieldName: '합류식(토실)', header: { text: '합류식' } }, - { name: 'CMBN_SOILHOLE_SITE', fieldName: '우수토실(합류)', header: { text: '우수토실(합류)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPRT_SOILHOLE_SITE', fieldName: '우수토실(분류)', header: { text: '우수토실(분류)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPRT_SOILSPLL_SITE', fieldName: '우수토구(분류)', header: { text: '우수토구(분류)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - { name: 'BRNCHLN_SM', fieldName: '합계(지선)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'CMBN_BRNCHLN_EXTN', fieldName: '합류식(지선)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'RAINWTR_BRNCHLN_EXTN', fieldName: '우수(지선)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'PLTWTR_BRNCHLN_EXTN', fieldName: '오수(지선)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - '하수처리장명', 'FCT_CPC', '구분', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['PROC_ZONE_SM', 'CMBN_PROC_ZONE_AR', 'SPRT_PROC_ZONE_AR'] }, - 'SEP_RATE', - { - name: 'drainGroup', header: { text: '배수설비(개소)' }, - items: ['DRNG_EQP_SM', 'CMBN_DRNG_EQP', { name: 'sepDrainGroup', header: { text: '분류식' }, items: ['SPRT_DRNG_EQP_SM', 'SPRT_DRNG_EQP_CNNC', 'SPRT_DRNG_EQP_DCNNC'] }], - }, - { - name: 'pipeGroup', header: { text: '하수관로' }, - items: [ - 'PIPL_FCLTY_EXTN', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['ICG_MNN_SM', 'ICG_EXTN', 'MNN_EXTN'] }, - { - name: 'siteGroup', header: { text: '토실/토구(개소)' }, - items: [ - { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['CMBN_SOILHOLE_SITE'] }, - { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['SPRT_SOILHOLE_SITE', 'SPRT_SOILSPLL_SITE'] } - ] - }, - { - name: 'branchGroup', header: { text: '지선(m)' }, - items: ['BRNCHLN_SM', 'CMBN_BRNCHLN_EXTN', { name: 'sepBranchGroup', header: { text: '분류식' }, items: ['RAINWTR_BRNCHLN_EXTN', 'PLTWTR_BRNCHLN_EXTN'] }] + { + tabKey: 'b', + tabLabel: '시설별 설치현황', + rowStartNum: 4, + fixedColumns: 3, + columns: [ + { + name: 'sido', + fieldName: 'sido', + header: { text: '시도' }, + mergeCells: true, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; } - ], - }, - 'RMK' - ] - }, + }, + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + } + ] + }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + } + ] + }, + { name: 'fctCpc', fieldName: 'fctCpc', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + /* 처리구역면적(㎢) */ + { name: 'procZoneSm', fieldName: 'procZoneSm', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cmbnProcZoneAr', fieldName: 'cmbnProcZoneAr', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sprtProcZoneAr', fieldName: 'sprtProcZoneAr', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sepRate', fieldName: 'sepRate', header: { text: '분류식화(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + /* 배수설비(개소) */ + { name: 'drngEqpSm', fieldName: 'drngEqpSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cmbnDrngEqp', fieldName: 'cmbnDrngEqp', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprtDrngEqpSm', fieldName: 'sprtDrngEqpSm', header: { text: '합계(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprtDrngEqpCnnc', fieldName: 'sprtDrngEqpCnnc', header: { text: '연결' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprtDrngEqpDcnnc', fieldName: 'sprtDrngEqpDcnnc', header: { text: '미연결' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 하수관로 */ + { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'icgMnnSm', fieldName: 'icgMnnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'icgExtn', fieldName: 'icgExtn', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'mnnExtn', fieldName: 'mnnExtn', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 토실/토구(개소) */ + { name: 'cmbnSoilholeSite', fieldName: 'cmbnSoilholeSite', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprtSoilholeSite', fieldName: 'sprtSoilholeSite', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprtSoilspllSite', fieldName: 'sprtSoilspllSite', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 지선(m) */ + { name: 'brnchlnSm', fieldName: 'brnchlnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnBrnchlnExtn', fieldName: 'cmbnBrnchlnExtn', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rainwtrBrnchlnExtn', fieldName: 'rainwtrBrnchlnExtn', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'pltwtrBrnchlnExtn', fieldName: 'pltwtrBrnchlnExtn', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'bscFctNm', + ] + }, + + 'fctCpc', + 'displayYear', + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['procZoneSm', 'cmbnProcZoneAr', 'sprtProcZoneAr'] }, + 'sepRate', + { + name: 'drainGroup', header: { text: '배수설비(개소)' }, + items: ['drngEqpSm', 'cmbnDrngEqp', { name: 'sepDrainGroup', header: { text: '분류식' }, items: ['sprtDrngEqpSm', 'sprtDrngEqpCnnc', 'sprtDrngEqpDcnnc'] }], + }, + { + name: 'pipeGroup', header: { text: '하수관로' }, + items: [ + 'piplFcltyExtn', + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['icgMnnSm', 'icgExtn', 'mnnExtn'] }, + { + name: 'siteGroup', header: { text: '토실/토구(개소)' }, + items: [ + { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cmbnSoilholeSite'] }, + { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprtSoilholeSite', 'sprtSoilspllSite'] } + ] + }, + { + name: 'branchGroup', header: { text: '지선(m)' }, + items: ['brnchlnSm', 'cmbnBrnchlnExtn', { name: 'sepBranchGroup', header: { text: '우수/오수' }, items: ['rainwtrBrnchlnExtn', 'pltwtrBrnchlnExtn'] }] + } + ], + }, + 'rmk' + ] +}, { tabKey: 'c', tabLabel: '관종별 현황', 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 b566d19c3..89718aaf0 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -6,12 +6,15 @@ import { type StatsInputStatusReviewApiResponse } from "../model/statistics-input-review-interface"; import { - type SewagePipeReviewApiResponse + type SewagePipeReviewRegionalApiResponse } from "../model/statistics-sewage-pipe-review-interface"; import { type SewerageSupplyRateReviewRequestParams, type SewerageSupplyRateReviewApiResponse, } from "../model/statistics-sewerage-penetration-review-interface"; +import { + type SewagePipeFacilityApiResponse +} from "../model/statistics-sewage-pipe-review-interface"; export class StatisticsManagementApiService { /** @@ -58,16 +61,34 @@ export class StatisticsManagementApiService { * 02. 하수관로 지역별 시설현황 목록 조회 (Tab1) * @param params (sido, gugun, yr, bsnmCd) */ - public async getSewagePipeReviewList( + public async getSewagePipeReviewRegionalList( params: { yr?: string; bsnmCd?: string; sido?: string; gugun?: string; } - ): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/pipe/tab1`, + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/regional-status`, + { params } + ); + return response.data; + } + /** + * 02. 하수관로 시설별 설치현황 목록 조회 (Tab2) + * @param params (sido, gugun, yr, bsnmCd) + */ + public async getSewagePipeFacilityList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/facility-status`, { params } ); return response.data; diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts index 581d31ba1..0d82096ad 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts @@ -1,9 +1,9 @@ import type { ApiResponse } from "./statistics-interface"; /** - * 02. 하수관로 지역별 시설현황 아이템 인터페이스 (Tab1) + * 02. 하수관로 tab1.지역별 시설현황 아이템 인터페이스 (Tab1) */ -export interface SewagePipeReviewItem { +export interface SewagePipeReviewrRegionalItem { // 기본 정보 및 행 제어 sido: string; // 시도 gugun: string; // 시군구 @@ -64,11 +64,11 @@ export interface SewagePipeReviewItem { } export interface SewagePipeReviewResponseData { - sewagePipeList: SewagePipeReviewItem[]; + sewagePipeList: SewagePipeReviewrRegionalItem[]; } -export type SewagePipeReviewApiResponse = ApiResponse & { +export type SewagePipeReviewRegionalApiResponse = ApiResponse & { message: string; }; @@ -79,5 +79,74 @@ export interface SewagePipeReviewService { bsnmCd?: string; // 사업장 코드 sido?: string; // 시도 gugun?: string; // 구군 - }): Promise; + }): Promise; +} + + +export interface SewagePipeReviewFacilityItem { + // 1. 행정구역 및 기본 정보 + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + adstDong: string; // 행정구역명 + yr: string; // 연도 + rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 + displayYear: string; // 구분 표시용 (2024년확정 등) + + // 2. 시설 정보 + bscFctNm: string; // 하수처리장명 + fctCpc: number; // 하수처리시설용량(㎥/일) + + // 3. 처리구역면적(㎢) + procZoneSm: number; // 합계(면적) + cmbnProcZoneAr: number; // 합류식(면적) + sprtProcZoneAr: number; // 분류식(면적) + sepRate: number; // 분류식화 (%) + + // 4. 배수설비(개소) + drngEqpSm: number; // 합계(배수) + cmbnDrngEqp: number; // 합류식(배수) + sprtDrngEqpSm: number; // 합계(분류배수) + sprtDrngEqpCnnc: number; // 연결 + sprtDrngEqpDcnnc: number; // 미연결 + + // 5. 하수관로 (m) + piplFcltyExtn: number; // 총 시설연장(m) + icgMnnSm: number; // 합계(차집/오수간선) + icgExtn: number; // 차집 + mnnExtn: number; // 오수간선 + + // 6. 토실/토구 (개소) + cmbnSoilholeSite: number; // 우수토실(합류) + sprtSoilholeSite: number; // 우수토실(분류) + sprtSoilspllSite: number; // 우수토구(분류) + + // 7. 지선 (m) + brnchlnSm: number; // 합계(지선) + cmbnBrnchlnExtn: number; // 합류식(지선) + rainwtrBrnchlnExtn: number; // 우수(지선) + pltwtrBrnchlnExtn: number; // 오수(지선) + + // 비고 + rmk: string; // 비고 +} + + +export interface SewagePipeFacilityResponseData { + facilityPipeList: SewagePipeReviewFacilityItem[]; +} + +export type SewagePipeFacilityApiResponse = ApiResponse & { + message: string; +}; + + +export interface SewagePipeFacilityService { + + getSewagePipeFacilityList(params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업장 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + }): Promise; } \ No newline at end of file From 12bed29eb9216a2d205fd10745137d9840b425a9 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 12 Feb 2026 13:42:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20any=20type=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../st/src/components/statistics/review/SewagePipeRvw.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index ece5e9718..1ba9d048f 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -16,7 +16,7 @@ import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 검토 02.하수관로 const SewagePipeRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - const [rowDataMap, setRowDataMap] = useState>(new Map()); + const [rowDataMap, setRowDataMap] = useState[]>([]); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState('a'); @@ -31,7 +31,7 @@ const SewagePipeRvw = () => { })); }, []); - const handleReset = () => setRowDataMap(new Map()); + const handleReset = () => setRowDataMap([]); const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === 'ALL') { @@ -53,12 +53,16 @@ const SewagePipeRvw = () => { if (activeTab === 'a') { response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); + } else if (activeTab === 'b') { + response = await statisticsManagementApi.getSewagePipeFacilityList(searchParam); } if (response?.status === 'success' && response.data) { const rawList = response.data; const newRowDataMap = new Map(); + setRowDataMap() + newRowDataMap.set(activeTab, toGridRows(rawList)); setRowDataMap(newRowDataMap); From 2873d1b0b9680a22b37e63534725b710b60e4350 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 12 Feb 2026 16:40:03 +0900 Subject: [PATCH 4/4] =?UTF-8?q?st:=20fix=20&=20refactor:=20TabDataGrid?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=85=9D=20=EC=A0=84=ED=99=98=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=ED=8F=AC=EB=A7=B7=20=EB=B0=8F=20footer=20?= =?UTF-8?q?=EC=82=AC=EB=9D=BC=EC=A7=80=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=ED=86=B5=EA=B3=84=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=20-=20=ED=95=98=EC=88=98=20=EA=B4=80=EB=A1=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/TabDataGrid.tsx | 122 ++++++++++++++++-- .../input/SeweragePenetrationRateInp.tsx | 1 + .../statistics/review/SewagePipeRvw.tsx | 81 ++++++++---- 3 files changed, 168 insertions(+), 36 deletions(-) diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index a7e0e3383..71d94fad3 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -656,22 +656,115 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }, [rowDataMap, columnInfo]); - // 활성 탭이 변경될 때마다 fitStyle 재적용 (그리드가 보일 때) + // 탭 전환 시 그리드 레이아웃·푸터·포맷 재적용 (여러 시점 + ResizeObserver) + const appliedTabRef = useRef(null); + useEffect(() => { - const gridRef = gridRefs.current.get(activeTab); - if (!gridRef?.gridView) return; + appliedTabRef.current = null; + const currentInfo = columnInfo.find(info => info.tabKey === activeTab); + const columns = currentInfo?.columns; + const columnLayout = currentInfo?.columnLayout; + if (!columns?.length || !columnLayout) return; - const gridView = gridRef.gridView; - // 약간의 지연을 두고 설정 (그리드 렌더링 완료 후) - const timer = setTimeout(() => { - gridView.setDisplayOptions({ - fitStyle: GridFitStyle.EVEN + const tryApply = () => { + if (appliedTabRef.current === activeTab) return; + const gridRef = gridRefs.current.get(activeTab); + if (!gridRef?.gridView || !gridRef?.dataProvider) return; + + const gridView = gridRef.gridView; + const dp = gridRef.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record[]) => void } | undefined; + const tabRowData = rowDataMap.get(activeTab) || []; + + try { + if (dp?.setFields) dp.setFields(buildFieldsFromColumns(columns)); + + gridView.setDisplayOptions({ fitStyle: GridFitStyle.EVEN }); + gridView.setColumnLayout?.(columnLayout as ColumnLayoutItem[]); + + const hasFooter = columns.some(col => col.footer); + if (hasFooter) { + const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer)); + const hasMultiRowFooter = columns.some(col => { + const f = col.footer; + return f && !Array.isArray(f) && 'multiRow' in f && (f as { multiRow?: boolean }).multiRow === true; + }); + if (hasArrayFooter || hasMultiRowFooter) { + const lengths = columns.filter(col => col.footer && Array.isArray(col.footer)).map(col => (col.footer as unknown[]).length); + const maxLen = lengths.length ? Math.max(...lengths) : 2; + gridView.setFooters?.(Array.from({ length: maxLen }, () => ({ height: 40 }))); + } else { + gridView.footer.height = 40; + } + } + gridView.setOptions?.({ summaryMode: "aggregate" }); + + if (columns.some(col => col.footer && Array.isArray(col.footer))) { + const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => { + const c: RealGridColumnInfo = { + name: col.name, + fieldName: col.fieldName, + header: col.header, + editable: col.editable, + valueType: col.valueType, + dataFormat: col.dataFormat, + ...(col.styleName != null && { styleName: col.styleName }), + ...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }), + }; + if (col.footer) { + if (Array.isArray(col.footer)) c.footer = col.footer; + else c.footer = { text: col.footer.text, expression: col.footer.expression, numberFormat: col.footer.numberFormat }; + } + return c; + }); + (gridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter); + columns.forEach(col => { + if (!col.footer || !Array.isArray(col.footer)) return; + try { + const column = gridView.columnByName(col.name); + const footerArr = column?.footer as unknown[] | undefined; + if (footerArr && Array.isArray(footerArr)) + col.footer.forEach((item, i) => { + if (item.valueCallback && footerArr[i]) + (footerArr[i] as FooterItemWithCallback).valueCallback = item.valueCallback; + }); + } catch (_) {} + }); + } + columns.forEach(col => { + if (!col.footer || Array.isArray(col.footer)) return; + try { + const column = gridView.columnByName(col.name); + const fo = col.footer as { valueCallback?: (g: unknown, col: unknown, fi: number, cf: unknown, v: unknown) => unknown }; + if (column?.footer && fo.valueCallback) + (column.footer as { valueCallback?: typeof fo.valueCallback }).valueCallback = fo.valueCallback; + } catch (_) {} + }); + + if (dp?.setRows && tabRowData.length) dp.setRows(tabRowData); + gridView.refresh(); + appliedTabRef.current = activeTab; + } catch (e) { + console.warn('TabDataGrid activeTab layout re-apply:', e); + } + }; + + const delays = [200, 500, 1000, 1500]; + const timers = delays.map(d => setTimeout(tryApply, d)); + + const container = document.querySelector(`[data-tab-key="${activeTab}"]`); + let observer: ResizeObserver | null = null; + if (container && typeof ResizeObserver !== 'undefined') { + observer = new ResizeObserver(() => { + if ((container as HTMLElement).offsetWidth > 0) tryApply(); }); - gridView.refresh(); - }, 50); + observer.observe(container); + } - return () => clearTimeout(timer); - }, [activeTab]); + return () => { + timers.forEach(t => clearTimeout(t)); + observer?.disconnect(); + }; + }, [activeTab, columnInfo, rowDataMap]); if (columnInfo.length === 0) { return
컬럼 정보가 없습니다.
; @@ -791,7 +884,9 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }
+ {activeTab === tabKey ? ( { if (el) { gridRefs.current.set(tabKey, el); @@ -1026,7 +1121,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement); (gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler; - }, 0); + }, 150); } } else { gridRefs.current.delete(tabKey); @@ -1061,6 +1156,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r /> ))} + ) : null}
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 70d4f10ae..9333b92cb 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -89,6 +89,7 @@ const SeweragePenetrationRateInp = () => { ...d, sido: commonInfo.sido, speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr, + piplSupprt: commonInfo.piplSupprt, })); setRowData(toGridRows(convertedData)); diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 1ba9d048f..34fe00910 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, act } from "react"; +import { useState, useMemo, useRef } from "react"; import TabDataGrid from "@/components/module/TabDataGrid"; import SearchForm from "@/components/module/SearchForm"; import SaveButton from "@/components/button/SaveButton"; @@ -10,20 +10,23 @@ import { LoadingWithAnimation } from "@common/components"; import { useAlertStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn"; +import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 검토 02.하수관로 const SewagePipeRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - const [rowDataMap, setRowDataMap] = useState[]>([]); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState('a'); + const searchParamRef = useRef(null); + const columnInfo = useMemo(() => { - return SewagePipeColumnInfoListRvw.map((tab: any) => ({ + return SewagePipeColumnInfoListRvw.map((tab: ColumnInfoList) => ({ ...tab, - columns: tab.columns.map((col: any) => ({ + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ ...col, editable: false as const, styleName: col.styleName ?? ("rg-cell-readonly" as const), @@ -31,7 +34,10 @@ const SewagePipeRvw = () => { })); }, []); - const handleReset = () => setRowDataMap([]); + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab('a'); + }; const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === 'ALL') { @@ -39,6 +45,10 @@ const SewagePipeRvw = () => { return; } + setActiveTab('a'); + + setRowDataMap(new Map()); + try { setIsLoading(true); @@ -47,37 +57,62 @@ const SewagePipeRvw = () => { gugun: values.gugun === 'ALL' ? '' : values.gugun, bsnmCd: values.bsnmCd, yr: values.year, - } + }; - let response; - - if (activeTab === 'a') { - response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); - } else if (activeTab === 'b') { - response = await statisticsManagementApi.getSewagePipeFacilityList(searchParam); - } + searchParamRef.current = searchParam; + const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); if (response?.status === 'success' && response.data) { - const rawList = response.data; - const newRowDataMap = new Map(); - - setRowDataMap() - - newRowDataMap.set(activeTab, toGridRows(rawList)); - + const newRowDataMap = new Map[]>(); + newRowDataMap.set('a', toGridRows(response.data as object[])); setRowDataMap(newRowDataMap); } else { alert(response?.message || "데이터가 없습니다."); } } catch (error) { - console.error("조회 에러:", error); alert('데이터 조회 중 오류가 발생했습니다.'); - handleReset(); } finally { setIsLoading(false); } }; + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const existingAData = rowDataMap.get('a'); + 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); + + let response; + + if (tabKey === 'b') { + response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current); + } + + if (response?.status === 'success' && response.data) { + setRowDataMap(prev => { + const next = new Map(prev); + next.set(tabKey, toGridRows(response.data as object[])); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } + }; + return (
@@ -104,7 +139,7 @@ const SewagePipeRvw = () => { rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} activeTab={activeTab} - onActiveTabChange={setActiveTab} + onActiveTabChange={handleActiveTabChange} />
);