From 4b883a30c534f4639eda495c2446ff5a64f5e427 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 17:47:28 +0900 Subject: [PATCH] =?UTF-8?q?feat:=2003.=ED=95=98=EC=88=98=EA=B4=80=EB=A1=9C?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80=EA=B4=80=EB=A6=AC=20tab1=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/SewagePipeMaintainRvw.tsx | 158 ++++++++++++++--- .../review/SewagePipeMaintainRvwColumn.ts | 162 ++++++++++++++---- .../api/statistics-management-service.ts | 22 +++ ...istics-sewage-pipe-mtc-review-interface.ts | 46 +++++ 4 files changed, 328 insertions(+), 60 deletions(-) create mode 100644 apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index 03549083e..8d66c5e0f 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -1,51 +1,159 @@ -import { useState } from "react"; - -import { TabDataGrid } from "@rsone/components"; +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 { useAlertStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn"; +import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 03. 하수관로 유지 관리 +// 하수도 통계 검토 03. 하수관로 유지 관리 const SewagePipeMaintainRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState('a'); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + + const columnInfo = useMemo(() => { + return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab('a'); + searchParamRef.current = null; }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === 'ALL') { + alert('시도를 선택해주세요.'); + return; + } + + setActiveTab('a'); + setRowDataMap(new Map()); + + try { + setIsLoading(true); + + const searchParam = { + sido: values.sido, + gugun: values.gugun === 'ALL' ? '' : values.gugun, + bsnmCd: values.bsnmCd, + yr: values.year, + }; + + searchParamRef.current = searchParam; + const response = await statisticsManagementApi.getSewerPipeMtcList(searchParam); + + if (response?.status === 'success' && response.data) { + const newRowDataMap = new Map[]>(); + + // API 응답이 배열을 직접 반환하는 경우와 객체로 감싸진 경우 모두 처리 + const listData = Array.isArray(response.data) + ? response.data + : (response.data.mtcStatusList || []); + + newRowDataMap.set('a', toGridRows(listData as object[])); + setRowDataMap(newRowDataMap); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch (error) { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 - }; + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + 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 === 'a') { + response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current); + } + + if (response?.status === 'success' && response.data) { + setRowDataMap(prev => { + const next = new Map(prev); + + // API 응답이 배열을 직접 반환하는 경우와 객체로 감싸진 경우 모두 처리 + const listData = Array.isArray(response.data) + ? response.data + : (response.data.mtcStatusList || []); + + next.set(tabKey, toGridRows(listData as object[])); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } }; return ( - <> - +
+ +
- - - - + { }} /> + { }} /> + { }} /> + { }} />
- - + + {isLoading && ( +
+
+ +
+
+ )} + + +
); }; diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index 28aa25ce5..b1e7b9f89 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -1,64 +1,156 @@ import { ValueType } from "realgrid"; +import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; +import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; // 하수도 통계 관리 03. 하수관로 유지 관리 const rawMaintainList = [ - { + { 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: '구분' } }, + /* 1. 행정구역 (시도) */ + { + 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] + ' 합계' : '합계'; + } + }, + { + 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 }, + + /* 2. 구분 (displayYear) */ + { + 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] + ' 합계' : ''; + } + } + ] + }, - /* 2. 전체보수 (단위: m) */ - { name: 'RPAIR_PIPE_SM', fieldName: '계(전체보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_CMBN_PIPE', fieldName: '합류식(전체보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_RAINWTR_PIPE', fieldName: '우수(전체보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_PLTWTR_PIPE', fieldName: '오수(전체보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 3. 전체보수 (m) - 소수점 1자리 합계 */ + { name: 'rpairPipeSm', fieldName: 'rpairPipeSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rpairCmbnPipe', fieldName: 'rpairCmbnPipe', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rpairRainwtrPipe', fieldName: 'rpairRainwtrPipe', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rpairPltwtrPipe', fieldName: 'rpairPltwtrPipe', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 3. 부분보수 (단위: 개소) */ - { name: 'RPAIR_PIPE_SM2', fieldName: '계(부분보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_CMBN_PIPE2', fieldName: '합류식(부분보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_RAINWTR_PIPE2', fieldName: '우수(부분보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RPAIR_PLTWTR_PIPE2', fieldName: '오수(부분보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 4. 부분보수 (개소) - 정수 합계 */ + { name: 'rpairPipeSm2', fieldName: 'rpairPipeSm2', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rpairCmbnPipe2', fieldName: 'rpairCmbnPipe2', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rpairRainwtrPipe2', fieldName: 'rpairRainwtrPipe2', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rpairPltwtrPipe2', fieldName: 'rpairPltwtrPipe2', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 4. 부속시설 (개소) */ - { name: 'MNHLE_SITE', fieldName: '합류식 맨홀', header: { text: '합류식\n맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLTWTR_MNHLE_SITE', fieldName: '오수맨홀', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RAINWTR_MNHLE_SITE', fieldName: '우수맨홀', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLTWTR_RCVR_SITE', fieldName: '오수받이(개소)', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RAINWTR_RCVR_SITE', fieldName: '빗물받이(개소)', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SOILHOLE_SITE', fieldName: '토실(개소)', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SOILSPLL_SITE', fieldName: '토구(개소)', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 5. 부속시설 (개소) - 정수 합계 */ + { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '합류식\n맨홀' }, 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' }, - /* 5. 조사량 및 비고 */ - { name: 'CCTV', fieldName: '수밀검사·CCTV조사량(m)', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + /* 6. 조사량 및 비고 */ + { name: 'cctv', fieldName: 'cctv', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + 'gugun' + ] + }, + 'displayYear', { name: 'mtcPipeGroup', header: { text: '개·보수관로' }, items: [ { name: 'totalRepairGroup', header: { text: '전체보수(m)' }, - items: ['RPAIR_PIPE_SM', 'RPAIR_CMBN_PIPE', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE', 'RPAIR_PLTWTR_PIPE'] }] + items: ['rpairPipeSm', 'rpairCmbnPipe', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['rpairRainwtrPipe', 'rpairPltwtrPipe'] }] }, { name: 'partRepairGroup', header: { text: '부분보수(개소)' }, - items: ['RPAIR_PIPE_SM2', 'RPAIR_CMBN_PIPE2', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE2', 'RPAIR_PLTWTR_PIPE2'] }] + items: ['rpairPipeSm2', 'rpairCmbnPipe2', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['rpairRainwtrPipe2', 'rpairPltwtrPipe2'] }] } ] }, - 'MNHLE_SITE', - { name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['PLTWTR_MNHLE_SITE', 'RAINWTR_MNHLE_SITE'] }, - 'PLTWTR_RCVR_SITE', 'RAINWTR_RCVR_SITE', - { name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['SOILHOLE_SITE', 'SOILSPLL_SITE'] }, - 'CCTV', 'RMK' + 'mnhleSite', + { name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['pltwtrMnhleSite', 'rainwtrMnhleSite'] }, + 'pltwtrRcvrSite', 'rainwtrRcvrSite', + { name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['soilspllSite', 'soilholeSite'] }, + 'cctv', 'rmk' ] }, { 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 4413a8e1a..30ff9fb98 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -15,6 +15,9 @@ import { type SewerPipeTypeApiResponse, type SewerPipeDiamApiResponse } from "../model/statistics-sewage-pipe-review-interface"; +import { + type SewerPipeMtcApiResponse +} from "../model/statistics-sewage-pipe-mtc-review-interface"; export class StatisticsManagementApiService { @@ -132,6 +135,25 @@ export class StatisticsManagementApiService { ); return response.data; } + + /** + * 03. 하수관로 유지관리 개보수 현황 조회 + * @param params (yr, bsnmCd, sido, gugun) + */ +public async getSewerPipeMtcList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } +): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/repair`, + { params } + ); + return response.data; +} } // 통합 인스턴스 수출 diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts new file mode 100644 index 000000000..5bab50b28 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts @@ -0,0 +1,46 @@ +import type { ApiResponse } from "./statistics-interface"; + +/** + * 03. 하수관로 유지관리 개보수 아이템 + */ +export interface SewerPipeMtcItem { + // 1. 행정구역 및 기본정보 + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + adstDong?: string; // 행정동 + yr: string; // 실제 연도 + displayYear: string; // 화면 표시용 (예: 2023년확정) + rowType: 'OLD' | 'CURR'; // OLD / CURR 구분 + + // 2. 개보수 관로 - 전체보수(m) + rpairPipeSm: number; // 계 + rpairCmbnPipe: number; // 합류식 + rpairRainwtrPipe: number; // 분류식-우수 + rpairPltwtrPipe: number; // 분류식-오수 + + // 3. 개보수 관로 - 부분보수(개소) + rpairPipeSm2: number; // 계 + rpairCmbnPipe2: number; // 합류식 + rpairRainwtrPipe2: number; // 분류식-우수 + rpairPltwtrPipe2: number; // 분류식-오수 + + // 4. 부속 시설물 (개소/m) + mnhleSite: number; // 합류식 맨홀 + pltwtrMnhleSite: number; // 오수 맨홀 (분류식) + rainwtrMnhleSite: number; // 우수 맨홀 (분류식) + pltwtrRcvrSite: number; // 오수받이 + rainwtrRcvrSite: number; // 빗물받이 + soilspllSite: number; // 토실 + soilholeSite: number; // 토구 + cctv: number; // CCTV 조사량 (m) + + // 5. 기타 및 상태값 + rmk: string; // 비고 +} + +export interface SewerPipeMtcResponseData { + mtcStatusList: SewerPipeMtcItem[]; +} + +export type SewerPipeMtcApiResponse = ApiResponse; \ No newline at end of file