From ca83ac22aa9e70a7329b98cc849be4dd3893eaa7 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 15:03:38 +0900 Subject: [PATCH 001/113] =?UTF-8?q?feat:=2002.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20tab3=EA=B4=80=EC=A2=85=EB=B3=84=20=ED=98=84?= =?UTF-8?q?=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/SewagePipeRvw.tsx | 3 + .../util/column/review/SewagePipeRvwColumn.ts | 166 +++++++++++++----- .../api/statistics-management-service.ts | 27 ++- ...statistics-sewage-pipe-review-interface.ts | 47 +++++ 4 files changed, 199 insertions(+), 44 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 34fe00910..f8129ad85 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -94,9 +94,12 @@ const SewagePipeRvw = () => { if (tabKey === 'b') { response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current); + }else if(tabKey === 'c'){ + response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current); } if (response?.status === 'success' && response.data) { + setRowDataMap(prev => { const next = new Map(prev); next.set(tabKey, toGridRows(response.data as object[])); diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 7bcb3a41f..a6e3d79b4 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -56,7 +56,7 @@ const rawList = [ ] }, { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false }, + { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true,editable: false }, { name: 'displayYear', fieldName: 'displayYear', @@ -235,7 +235,7 @@ const rawList = [ ] }, { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false }, + { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' },mergeCells: true, editable: false }, { name: 'displayYear', fieldName: 'displayYear', @@ -351,47 +351,133 @@ const rawList = [ 'rmk' ] }, - { - tabKey: 'c', - tabLabel: '관종별 현황', - rowStartNum: 2, - fixedColumns: 3, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'SWRG_PIPE_NM', fieldName: '관종', header: { text: '관종' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, + { + tabKey: 'c', + tabLabel: '관종별 현황', + rowStartNum: 2, + 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 uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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 uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true, 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 uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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 uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue) uniqueValues.add(String(divisionValue)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + } + ] + }, - { name: 'TOTAL_EXTN_SUM', fieldName: '계(총계)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LAYING_EXTN_SUM', fieldName: '매설연장(m)_총계', header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'REMVL_EXTN_SUM', fieldName: '철거연장(m)_총계', header: { text: '철거연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 총계 필드 */ + { name: 'totalExtnSum', fieldName: 'totalExtnSum', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'layingExtnSum', fieldName: 'layingExtnSum', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'remvlExtnSum', fieldName: 'remvlExtnSum', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - const yrOffset = 29 - i; - return [ - { name: `TOTAL_EXTN_${yNum}Y`, fieldName: `계(-${yrOffset}년)`, header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `LAYING_EXTN_${yNum}Y`, fieldName: `매설연장(m)_(-${yrOffset}년)`, header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `REMVL_EXTN_${yNum}Y`, fieldName: `철거연장(m)_(-${yrOffset}년)`, header: { text: '철거연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } - ]; - }).flat(), + /* 30개년 동적 컬럼 생성 (SewerPipeTypeItem 필드 매핑) */ + ...Array.from({ length: 30 }, (_, i) => { + const yNum = (30 - i).toString().padStart(2, '0'); + const yrOffset = 29 - i; + return [ + { name: `totalExtn${yNum}y`, fieldName: `totalExtn${yNum}y`, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `layingExtn${yNum}y`, fieldName: `layingExtn${yNum}y`, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `remvlExtn${yNum}y`, fieldName: `remvlExtn${yNum}y`, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + ]; + }).flat(), - { name: 'LAYING_EXTN', fieldName: `매설연장(m)_당해년도`, header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SWRG_PIPE_NM', 'YR_GBN', - { name: 'summaryGroup', header: { text: '총계' }, items: ['TOTAL_EXTN_SUM', 'LAYING_EXTN_SUM', 'REMVL_EXTN_SUM'] }, - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - const yrOffset = 29 - i; - return { name: `yr${yNum}Group`, header: { text: `-${yrOffset}년` }, items: [`TOTAL_EXTN_${yNum}Y`, `LAYING_EXTN_${yNum}Y`, `REMVL_EXTN_${yNum}Y`] }; - }), - { name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['LAYING_EXTN'] }, - 'RMK' - ] - }, + { name: 'layingExtn', fieldName: 'layingExtn', header: { text: '매설연장(m)\n(당해년도)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'areaGroup', + header: { text: '행정구역' }, + items: [ + // sido부터 swrgPipeNm(관종)까지 푸터 병합 + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'swrgPipeNm', + ] + }, + 'displayYear', + { name: 'summaryGroup', header: { text: '총계' }, items: ['totalExtnSum', 'layingExtnSum', 'remvlExtnSum'] }, + + ...Array.from({ length: 30 }, (_, i) => { + const yNum = (30 - i).toString().padStart(2, '0'); + const yrOffset = 29 - i; + return { + name: `yr${yNum}Group`, + header: { text: `-${yrOffset}년` }, + items: [`totalExtn${yNum}y`, `layingExtn${yNum}y`, `remvlExtn${yNum}y`] + }; + }), + + { name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['layingExtn'] }, + 'rmk' + ] +}, { tabKey: 'd', 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 89718aaf0..5fc81ab34 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -5,17 +5,17 @@ import { ENDPOINT } from "@/shared/config"; import { type StatsInputStatusReviewApiResponse } from "../model/statistics-input-review-interface"; -import { - type SewagePipeReviewRegionalApiResponse -} from "../model/statistics-sewage-pipe-review-interface"; import { type SewerageSupplyRateReviewRequestParams, type SewerageSupplyRateReviewApiResponse, } from "../model/statistics-sewerage-penetration-review-interface"; import { - type SewagePipeFacilityApiResponse + type SewagePipeReviewRegionalApiResponse , + type SewagePipeFacilityApiResponse, + type SewerPipeTypeApiResponse, } from "../model/statistics-sewage-pipe-review-interface"; + export class StatisticsManagementApiService { /** * 00. 통계 입력 현황 목록 조회 @@ -93,6 +93,25 @@ export class StatisticsManagementApiService { ); return response.data; } + + /** + * 02. 하수관로 관종별 시설현황 조회 (Tab2) + * @param params (sido, gugun, yr, bsnmCd) + */ + public async getSewagePipeTypeList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/type-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 0d82096ad..ea7500cf5 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 @@ -149,4 +149,51 @@ export interface SewagePipeFacilityService { sido?: string; // 시도 gugun?: string; // 구군 }): Promise; +} + +/** + * 02. 하수관로 관종별 시설현황 (PipeType) + * + */ +export interface SewerPipeTypeItem { + // --- SewerPipeTab3Response 기본 필드 --- + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + swrgPipeNm: string; // 관종명 + displayYear: string; // 구분 표시용 (예: 2024년확정) + yr: string; // 연도 + rmk: string; // 비고 + yrGbn?: string; // 구분 (매설/철거 등 행 구분용 - 그리드 설정 기준) + + // --- BasePipeExtnResponse 상속 필드 --- + layingExtn: number; // 매설연장(m) - 당해년도 + + // 총계(Sum) 필드 (그리드 설정에서 사용하는 합계용 필드) + totalExtnSum?: number; + layingExtnSum?: number; + remvlExtnSum?: number; + + // 01y ~ 30y 동적 필드 처리 (Index Signature) + // 예: totalExtn01y, layingExtn05y, remvlExtn30y 등 + [key: `totalExtn${string}y`]: number | undefined; + [key: `layingExtn${string}y`]: number | undefined; + [key: `remvlExtn${string}y`]: number | undefined; +} + + +export interface SewerPipeTypeResponseData { + pipeTypeStatusList: SewerPipeTypeItem[]; +} + +export type SewerPipeTypeApiResponse = ApiResponse; + +export interface SewagePipeTypeService { + + getSewagePipeTypeList(params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업장 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + }): Promise; } \ No newline at end of file From 37e3b6ebdb832d03f7d8feb50c42316f2de168e1 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 16:33:25 +0900 Subject: [PATCH 002/113] =?UTF-8?q?feat:=2002.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20tab4=EA=B4=80=EA=B2=BD=EB=B3=84=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= 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 | 155 ++++++++++++++---- .../api/statistics-management-service.ts | 22 ++- ...statistics-sewage-pipe-review-interface.ts | 46 +++++- 4 files changed, 193 insertions(+), 32 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index f8129ad85..ac7033e53 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -96,6 +96,8 @@ const SewagePipeRvw = () => { response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current); }else if(tabKey === 'c'){ response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current); + }else if(tabKey === 'd'){ + response = await statisticsManagementApi.getSewagePipeDiamList(searchParamRef.current); } if (response?.status === 'success' && response.data) { diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index a6e3d79b4..b57a54d44 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -436,7 +436,6 @@ const rawList = [ { name: 'layingExtnSum', fieldName: 'layingExtnSum', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'remvlExtnSum', fieldName: 'remvlExtnSum', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 30개년 동적 컬럼 생성 (SewerPipeTypeItem 필드 매핑) */ ...Array.from({ length: 30 }, (_, i) => { const yNum = (30 - i).toString().padStart(2, '0'); const yrOffset = 29 - i; @@ -455,7 +454,7 @@ const rawList = [ name: 'areaGroup', header: { text: '행정구역' }, items: [ - // sido부터 swrgPipeNm(관종)까지 푸터 병합 + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'gugun', 'swrgPipeNm', @@ -478,33 +477,131 @@ const rawList = [ 'rmk' ] }, - { - tabKey: 'd', - tabLabel: '관경별 현황', - rowStartNum: 2, - fixedColumns: 3, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '구군' } }, - { name: 'SWRG_PIPE_NM', fieldName: '관종', header: { text: '관종' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOTAL_EXTN_M', fieldName: '총계(m)', header: { text: '총계(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'UNDER_200', fieldName: '200mm 미만', header: { text: '200mm 미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OVER_200_UNDEV_300', fieldName: '200mm이상~300mm미만', header: { text: '200mm이상~\n300mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OVER_300_UNDEV_500', fieldName: '300mm이상~500mm미만', header: { text: '300mm이상~\n500mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OVER_500_UNDEV_800', fieldName: '500mm이상~800mm미만', header: { text: '500mm이상~\n800mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OVER_800', fieldName: '800mm 이상', header: { text: '800mm 이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BOX', fieldName: '사각형(m)', header: { text: '사각형(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타(측구/개거 등)', header: { text: '기타\n(측구/개거 등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SWRG_PIPE_NM', 'YR_GBN', 'TOTAL_EXTN_M', - { name: 'circleGroup', header: { text: '원형(m)' }, items: ['UNDER_200', 'OVER_200_UNDEV_300', 'OVER_300_UNDEV_500', 'OVER_500_UNDEV_800', 'OVER_800'] }, - 'BOX', 'ETC', 'RMK', - ] - } + { + tabKey: 'd', + tabLabel: '관경별 현황', + rowStartNum: 2, + 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] + '합계' : '합계'; + } + }, + { + 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: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true, 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: 'totalExtnM', fieldName: 'totalExtnM', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'under200', fieldName: 'under200', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over200Undev300', fieldName: 'over200Undev300', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over300Undev500', fieldName: 'over300Undev500', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over500Undev500', fieldName: 'over500Undev500', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over800', fieldName: 'over800', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'box', fieldName: 'box', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etc', fieldName: 'etc', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'swrgPipeNm', + ] + }, + 'displayYear', + 'totalExtnM', + { + name: 'circleGroup', + header: { text: '원형(m)' }, + items: ['under200', 'over200Undev300', 'over300Undev500', 'over500Undev500', 'over800'] + }, + 'box', + 'etc', + 'rmk' + ] +} ]; export const SewagePipeColumnInfoListRvw = rawList.map(tab => ({ 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 5fc81ab34..4413a8e1a 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -13,6 +13,7 @@ import { type SewagePipeReviewRegionalApiResponse , type SewagePipeFacilityApiResponse, type SewerPipeTypeApiResponse, + type SewerPipeDiamApiResponse } from "../model/statistics-sewage-pipe-review-interface"; @@ -95,7 +96,7 @@ export class StatisticsManagementApiService { } /** - * 02. 하수관로 관종별 시설현황 조회 (Tab2) + * 02. 하수관로 관종별 시설현황 조회 (Tab3) * @param params (sido, gugun, yr, bsnmCd) */ public async getSewagePipeTypeList( @@ -112,6 +113,25 @@ export class StatisticsManagementApiService { ); return response.data; } + + /** + * 02. 하수관로 관경별 시설현황 조회 (Tab 4) + * @param params (sido, gugun, yr, bsnmCd) + */ + public async getSewagePipeDiamList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/diam-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 ea7500cf5..6abb98a80 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 @@ -82,7 +82,10 @@ export interface SewagePipeReviewService { }): Promise; } - +/** + * 02. 하수관로 시설별 시설현황 (Tab 2) + * + */ export interface SewagePipeReviewFacilityItem { // 1. 행정구역 및 기본 정보 sido: string; // 시도 @@ -152,7 +155,7 @@ export interface SewagePipeFacilityService { } /** - * 02. 하수관로 관종별 시설현황 (PipeType) + * 02. 하수관로 관종별 시설현황 (Tab 3) * */ export interface SewerPipeTypeItem { @@ -196,4 +199,43 @@ export interface SewagePipeTypeService { sido?: string; // 시도 gugun?: string; // 구군 }): Promise; +} + +/** + * 02. 하수관로 관경별 시설현황 아이템 (Tab 4) + */ +export interface SewerPipeDiamItem { + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + swrgPipeNm: string; // 관종명 (또는 관경 관련 명칭) + displayYear: string; // 구분 (예: 2023년 확정 / 2024년) + + // 관경별 수치 데이터 + under200: number; // 200미만 + over200Undev300: number; // 200~300 + over300Undev500: number; // 300~500 + over500Undev500: number; // 500~800 (DTO 명칭 기준) + over800: number; // 800이상 + box: number; // BOX + etc: number; // 기타 + rmk: string; // 비고 +} + + +export interface SewerPipeDiamResponseData { + pipeDiamStatusList: SewerPipeDiamItem[]; +} + + +export type SewerPipeDiamApiResponse = ApiResponse; + + +export interface SewagePipeDiamService { + getSewagePipeDiamList(params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업장 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + }): Promise; } \ No newline at end of file From 4b883a30c534f4639eda495c2446ff5a64f5e427 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 17:47:28 +0900 Subject: [PATCH 003/113] =?UTF-8?q?feat:=2003.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9C=A0=EC=A7=80=EA=B4=80=EB=A6=AC=20tab1=20?= =?UTF-8?q?=EC=A1=B0=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 From be21ea3e8a087c1cd700353cb1e87ed64a2b7637 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 17:58:41 +0900 Subject: [PATCH 004/113] =?UTF-8?q?feat:=2003.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9C=A0=EC=A7=80=EA=B4=80=EB=A6=AC=20tab2?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=EC=A4=80=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/SewagePipeMaintainRvw.tsx | 2 + .../review/SewagePipeMaintainRvwColumn.ts | 187 ++++++++++++------ .../api/statistics-management-service.ts | 22 ++- ...istics-sewage-pipe-mtc-review-interface.ts | 55 +++++- 4 files changed, 204 insertions(+), 62 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index 8d66c5e0f..f2da6cce4 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -99,6 +99,8 @@ const SewagePipeMaintainRvw = () => { if (tabKey === 'a') { response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current); + }else if (tabKey === 'b') { + response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current); } if (response?.status === 'success' && response.data) { diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index b1e7b9f89..ab32f39a5 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -153,71 +153,138 @@ const rawMaintainList = [ 'cctv', 'rmk' ] }, - { - tabKey: 'b', - tabLabel: '관로준설', - rowStartNum: 4, - fixedColumns: 2, - columns: [ - /* 1. 행정 및 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 2. 시설 기본 정보 */ - { name: 'PIPL_FCLTY_EXTN', fieldName: '하수관로 총시설연장(m)', header: { text: '하수관로\n총시설연장\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { + tabKey: 'b', + tabLabel: '관로준설', + rowStartNum: 4, + fixedColumns: 2, + columns: [ + /* 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) 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) 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: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const uniqueValues = new Set(); + for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { + const val = grid.getDataSource().getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > 0 ? divisionValues[0] + ' 합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const uniqueValues = new Set(); + for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { + const val = grid.getDataSource().getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > 1 ? divisionValues[1] + ' 합계' : ''; + } + } + ] + }, + + /* 2. 시설 기본 정보 */ + { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 3. 준설연장 및 개소수 */ - { name: 'DRDG_PIPE_SM', fieldName: '합계(준설)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRDG_CMBN_PIPE', fieldName: '합류식(준설)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRDG_RAINWTR_PIPE', fieldName: '우수(준설)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRDG_PLTWTR_PIPE', fieldName: '오수(준설)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'MNHLE_SITE_DRDG', fieldName: '맨홀(개소)', header: { text: '맨홀\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLTWTR_RCVR_SITE_DRDG', fieldName: '오·우수받이(개소)', header: { text: '오·우수받이\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SOILSPLL_SOILHOLE_SITE', fieldName: '토실·토구(개소)', header: { text: '토실·토구\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 3. 준설연장 및 개소수 */ + { name: 'drdgPipeSm', fieldName: 'drdgPipeSm', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgCmbnPipe', fieldName: 'drdgCmbnPipe', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgRainwtrPipe', fieldName: 'drdgRainwtrPipe', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgPltwtrPipe', fieldName: 'drdgPltwtrPipe', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'soilspllSoilholeSite', fieldName: 'soilspllSoilholeSite', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 4. 준설 계획 및 율 */ - { name: 'DRDG_TRGET_EXTN', fieldName: '준설 계획 연장(m)', header: { text: '준설 계획\n연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRDG_TRGET_RT', fieldName: '계획대비 준설율(%)', header: { text: '계획대비\n준설율\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PIPL_FCLTY_RT', fieldName: '준설계획 연장대비 준설율(%)', header: { text: '준설계획\n연장대비\n준설율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 4. 준설 계획 및 율 */ + { name: 'drdgTrgetExtn', fieldName: 'drdgTrgetExtn', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgTrgetRt', fieldName: 'drdgTrgetRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'piplFcltyRt', fieldName: 'piplFcltyRt', header: { text: '준설계획\n연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 5. 준설실적 */ - { name: 'DRDG_ACMSLT_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DRDG_ACMSLT_QY', fieldName: '준설량(㎥)', header: { text: '준설량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 5. 준설실적 */ + { name: 'drdgAcmsltAmount', fieldName: 'drdgAcmsltAmount', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drdgAcmsltQy', fieldName: 'drdgAcmsltQy', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 6. 준설토사처리 */ - { name: 'DRDG_ACMSLT_UNTPC', fieldName: '준설토 처리비(백만원)', header: { text: '준설토\n처리비\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DRDG_ETND_SM', fieldName: '처리량 계(톤)', header: { text: '처리량 계\n(톤)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RCLM', fieldName: '매립', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타(준설)', header: { text: '기타\n(소각, 고형화,\n시멘트원료 등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 6. 준설토사처리 */ + { name: 'drdgAcmsltUntpc', fieldName: 'drdgAcmsltUntpc', header: { text: '준설토\n처리비\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drdgEtndSm', fieldName: 'drdgEtndSm', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rclm', fieldName: 'rclm', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'oceanDpsl', fieldName: 'oceanDpsl', header: { text: '해양투기' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'ruse', fieldName: 'ruse', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etc', fieldName: 'etc', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 7. 기타 및 의견 */ - { name: 'RMK_DRDG', fieldName: '비고(준설)', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'PIPL_FCLTY_EXTN', - { - name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, - items: [ - { - name: 'pipeLenGroup', header: { text: '관로(m)' }, - items: ['DRDG_PIPE_SM', 'DRDG_CMBN_PIPE', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['DRDG_RAINWTR_PIPE', 'DRDG_PLTWTR_PIPE'] }] - }, - 'MNHLE_SITE_DRDG', 'PLTWTR_RCVR_SITE_DRDG', 'SOILSPLL_SOILHOLE_SITE' - ] - }, - 'DRDG_TRGET_EXTN', 'DRDG_TRGET_RT', 'PIPL_FCLTY_RT', - { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['DRDG_ACMSLT_AMOUNT', 'DRDG_ACMSLT_QY'] }, - { - name: 'processGroup', header: { text: '준설토사처리' }, - items: ['DRDG_ACMSLT_UNTPC', 'DRDG_ETND_SM', 'RCLM', 'OCEAN_DPSL', 'RUSE', 'ETC'] - }, - 'RMK_DRDG' - ] - } + /* 7. 기타 및 의견 */ + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { name: 'addrGroup', header: { text: '행정구역' }, items: [{ column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'gugun'] }, + 'displayYear', 'piplFcltyExtn', + { + name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, + items: [ + { + name: 'pipeLenGroup', header: { text: '관로(m)' }, + items: ['drdgPipeSm', 'drdgCmbnPipe', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['drdgRainwtrPipe', 'drdgPltwtrPipe'] }] + }, + 'mnhleSite', 'pltwtrRcvrSite', 'soilspllSoilholeSite' + ] + }, + 'drdgTrgetExtn', 'drdgTrgetRt', 'piplFcltyRt', + { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgAcmsltAmount', 'drdgAcmsltQy'] }, + { + name: 'processGroup', header: { text: '준설토사처리' }, + items: ['drdgAcmsltUntpc', 'drdgEtndSm', 'rclm', 'oceanDpsl', 'ruse', 'etc'] + }, + 'rmk' + ] +} ]; export const SewagePipeMaintainColumnInfoList = rawMaintainList.map(tab => ({ 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 30ff9fb98..4702636a8 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -16,7 +16,8 @@ import { type SewerPipeDiamApiResponse } from "../model/statistics-sewage-pipe-review-interface"; import { - type SewerPipeMtcApiResponse + type SewerPipeMtcApiResponse, + type SewerPipeDrdgApiResponse } from "../model/statistics-sewage-pipe-mtc-review-interface"; @@ -154,6 +155,25 @@ public async getSewerPipeMtcList( ); return response.data; } + +/** + * 03. 하수관로 유지관리 관로준설 현황 조회 (Tab B) + * @param params (yr, bsnmCd, sido, gugun) + */ +public async getSewerPipeDrdgList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } +): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/dredging`, + { 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 index 5bab50b28..eb47c2de7 100644 --- 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 @@ -43,4 +43,57 @@ export interface SewerPipeMtcResponseData { mtcStatusList: SewerPipeMtcItem[]; } -export type SewerPipeMtcApiResponse = ApiResponse; \ No newline at end of file +export type SewerPipeMtcApiResponse = ApiResponse; + +/** + * 03. 하수관로 유지관리 관로준설 아이템 (Tab 2) + */ +export interface SewerPipeDrdgItem { + // 1. 기본 정보 + sido: string; // 시도 + gugun: string; // 시군구 + yr: string; // 연도 + displayYear: string; // 화면 표시용 (예: 2024년, 2023년확정) + rowType: 'OLD' | 'CURR'; // OLD / CURR 구분 + bsnmCd: string; // 사업소코드 + + // 2. 시설 현황 + piplFcltyExtn: number; // 관로연장 (m) + + // 3. 준설 실적 - 관로 (m) + drdgPipeSm: number; // 준설계 + drdgCmbnPipe: number; // 합류식 + drdgPltwtrPipe: number; // 분류식-오수 + drdgRainwtrPipe: number; // 분류식-우수 + + // 4. 준설 실적 - 부속시설 (개소) + mnhleSite: number; // 맨홀 + pltwtrRcvrSite: number; // 오수받이 + soilspllSoilholeSite: number; // 토실/토구 + + // 5. 준설 효율 및 성과 + drdgTrgetExtn: number; // 준설대상 연장 (m) + drdgTrgetRt: number; // 준설대상 대비 준설률 (%) + piplFcltyRt: number; // 시설연장 대비 준설률 (%) + drdgAcmsltQy: number; // 준설실적 - 양 (m3) + drdgAcmsltAmount: number; // 준설실적 - 금액 (천원) + drdgAcmsltUntpc: number; // 준설실적 - 단가 (원/m3) + + // 6. 준설토 처리 (톤) + drdgEtndSm: number; // 처리량 계 + rclm: number; // 매립 + oceanDpsl: number; // 해양투기 + ruse: number; // 재활용 + etc: number; // 기타 + + // 7. 기타 및 상태값 + rmk: string; // 비고 +} + + +export interface SewerPipeDrdgResponseData { + drdgStatusList: SewerPipeDrdgItem[]; +} + + +export type SewerPipeDrdgApiResponse = ApiResponse; \ No newline at end of file From 842ec36c322736295a7efb89f709fa1a65a1cb09 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 19 Feb 2026 15:41:19 +0900 Subject: [PATCH 005/113] =?UTF-8?q?feat:=20=ED=95=98=EC=88=98=20=EC=A0=80?= =?UTF-8?q?=EB=A5=98=EC=8B=9C=EC=84=A4=20=EB=B0=8F=20=EB=B9=97=EB=AC=BC=20?= =?UTF-8?q?=ED=8E=8C=ED=94=84=EC=9E=A5=20=EC=A1=B0=ED=9A=8C=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/SewageReservoirPumpRvw.tsx | 153 +++++- .../review/SewageReservoirPumpRvwColumn.ts | 448 ++++++++++++------ .../api/statistics-management-service.ts | 43 ++ .../sewage-reservoir-pump-review-interface.ts | 171 +++++++ 4 files changed, 640 insertions(+), 175 deletions(-) create mode 100644 apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index a2b5e8de7..3b7645a4e 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -1,52 +1,153 @@ -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 { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; +import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장 +/** + * 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장 + * Tab A: 하수저류시설 + * Tab B: 빗물펌프장 + */ const SewageReservoirPumpRvw = () => { + 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 SewageReservoirPumpInfoListRvw.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'); - // 저장 로직 + // 조회 버튼 클릭 시: Tab A(하수저류시설) 데이터 로드 + 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; + + //하수저류시설 (Tab A) + const response = await statisticsManagementApi.getSewerReservoirList(searchParam); + + if (response?.status === 'success' && response.data) { + const newRowDataMap = new Map[]>(); + newRowDataMap.set('a', toGridRows(response.data as object[])); + setRowDataMap(newRowDataMap); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch (error) { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 - }; + // 탭 전환 시: 데이터가 없는 경우 지연 로딩 (Tab B: 빗물펌프장) + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + // 메인 데이터(Tab A)가 있고, 현재 탭 데이터가 없을 때만 API 호출 + if (searchParamRef.current && !hasActiveTabData) { + try { + setIsLoading(true); + let response; + + if (tabKey === 'b') { + response = await statisticsManagementApi.getPumpStationList(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 if (response) { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } }; return ( - <> - +
+ +
- - - - + { /* 검토의견 로직 */ }} /> + { /* 승인 로직 */ }} /> + { /* 반려 로직 */ }} /> + { /* 저장 로직 */ }} />
- - + + {isLoading && ( +
+
+ +
+
+ )} + + +
); }; -export default SewageReservoirPumpRvw; +export default SewageReservoirPumpRvw; \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts index 43aa1c62d..b5caf5604 100644 --- a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts @@ -1,186 +1,336 @@ import { ValueType } from "realgrid"; +import { + footerIntArrayByDivision, + footerDecimal1ArrayByDivision, + footerDecimal3ArrayByDivision +} from "@/components/util/grid/footerSummary"; +import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; + // 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장 const rawReservoirList = [ { tabKey: 'a', - tabLabel: '하수저류시설', + tabLabel: '하수저류시설현황', rowStartNum: 3, fixedColumns: 2, columns: [ /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'SWGE_STRGE_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'ADDR', fieldName: '위치', header: { text: '위치' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 2. 시설 제원 */ - { name: 'RN_DT', fieldName: '가동개시 년월일', header: { text: '가동개시\n년월일' } }, - { name: 'PL_DSGN_FRQNC', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } }, - { name: 'INS_PER1', fieldName: '침수예방', header: { text: '침수예방' } }, - { name: 'INS_PER2', fieldName: '수질오염저감', header: { text: '수질오염저감' } }, - { name: 'INS_PER3', fieldName: '하수 재이용 용도', header: { text: '하수\n재이용\n용도' } }, - { name: 'FAC_RES', fieldName: '시설구조', header: { text: '시설구조(일반지하식,지하터널식,기타)' } }, - { name: 'DGR_AR', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'SWGE_SGBS_CPCTY', fieldName: '시설용량(㎥)', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 펌프시설 */ - { name: 'TOT_CPCTY', fieldName: '총양수량(㎥/min)', header: { text: '총양수량\n(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CALBR', fieldName: '구경(mm)_1', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY', fieldName: '대당 양수량_1', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO', fieldName: '펌프대수_1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO', fieldName: '예비펌프대수_1', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_CALBR2', fieldName: '구경(mm)_2', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY2', fieldName: '대당 양수량_2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO2', fieldName: '펌프대수_2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO2', fieldName: '예비펌프대수_2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_CALBR3', fieldName: '구경(mm)_3', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY3', fieldName: '대당 양수량_3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO3', fieldName: '펌프대수_3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO3', fieldName: '예비펌프대수_3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 부대시설 및 방식 */ - { name: 'FRE_FNNC', fieldName: '전처리시설(유/무)', header: { text: '전처리시설(유/무)' } }, - { name: 'DORZEQP_FNNC', fieldName: '탈취설비(유/무)', header: { text: '탈취설비(유/무)' } }, - { name: 'SELF_ETRQ_FNNC', fieldName: '자가발전(유/무)', header: { text: '자가발전(유/무)' } }, - { name: 'DRA_MTH', fieldName: '배수방식(자연배수,강제배수)', header: { text: '배수방식(자연배수,강제배수)' } }, - { name: 'ELEC_MTH', fieldName: '저류방식(직렬,병렬)', header: { text: '저류방식(직렬,병렬)' } }, - { name: 'ELEC_PRO_MAT', fieldName: '저류수 처리방법', header: { text: '저류수\n처리방법' } }, - { name: 'DIS_WAT', fieldName: '방류수역(또는 연계처리장명)', header: { text: '방류수역(또는 연계처리장명)' } }, - - /* 5. 최종처분량 및 방법 */ - { name: 'SDMT_TRE_CPCTY', fieldName: '침사(량)', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'IPRT_TRE_CPCTY', fieldName: '협잡물(량)', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SDMT_TRE_MTH', fieldName: '침사(방법)', header: { text: '침사' } }, - { name: 'IPRT_TRE_MTH', fieldName: '협잡물(방법)', header: { text: '협잡물' } }, - - /* 6. 운영실적 */ - { name: 'YRSTP_RN_DYN', fieldName: '가동일수', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DSPLCMNT_AMNT', fieldName: '총배수량(㎥/년)', header: { text: '총배수량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SWGE_STRGE_NM', 'ADDR', 'YR_GBN', 'RN_DT', 'PL_DSGN_FRQNC', { - name: 'purposeGroup', - header: { text: '설치목적(O,X로 표기, 중복체크 가능)' }, - items: ['INS_PER1', 'INS_PER2', 'INS_PER3'] - }, - 'FAC_RES', 'DGR_AR', 'SWGE_SGBS_CPCTY', - { - name: 'pumpMainGroup', - header: { text: '펌프시설' }, - items: [ - 'TOT_CPCTY', - { name: 'p1Group', header: { text: '펌프시설1' }, items: ['PUMP_CALBR', 'WP_CPCTY', 'PUMP_CO', 'PUMP_PREPAR_CO'] }, - { name: 'p2Group', header: { text: '펌프시설2' }, items: ['PUMP_CALBR2', 'WP_CPCTY2', 'PUMP_CO2', 'PUMP_PREPAR_CO2'] }, - { name: 'p3Group', header: { text: '펌프시설3' }, items: ['PUMP_CALBR3', 'WP_CPCTY3', 'PUMP_CO3', 'PUMP_PREPAR_CO3'] } + 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); + 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)); + } + } + 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: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true, editable: false }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true, editable: false }, { - name: 'subFacGroup', - header: { text: '부대시설' }, - items: ['FRE_FNNC', 'DORZEQP_FNNC', 'SELF_ETRQ_FNNC'] + 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] + '합계' : ''; + } + } + ] }, - 'DRA_MTH', 'ELEC_MTH', 'ELEC_PRO_MAT', 'DIS_WAT', + + /* 2. 시설 제원 */ + { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동개시 년월일' } }, + { name: 'plDsgnFrqnc', fieldName: 'plDsgnFrqnc', header: { text: '계획설계빈도' } }, + { name: 'insPer1', fieldName: 'insPer1', header: { text: '침수예방' } }, + { name: 'insPer2', fieldName: 'insPer2', header: { text: '수질오염저감' } }, + { name: 'insPer3', fieldName: 'insPer3', header: { text: '하수 재이용 용도' } }, + { name: 'facRes', fieldName: 'facRes', header: { text: '시설구조' } }, + { name: 'dgrAr', fieldName: 'dgrAr', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, + { name: 'swgeSgbsCpcty', fieldName: 'swgeSgbsCpcty', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + + /* 3. 펌프시설 */ + { name: 'totCpcty', fieldName: 'totCpcty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCalbr', fieldName: 'pumpCalbr', header: { text: '구경(mm)' } }, + { name: 'wpCpcty', fieldName: 'wpCpcty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo', fieldName: 'pumpCo', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo', fieldName: 'pumpPreparCo', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpCalbr2', fieldName: 'pumpCalbr2', header: { text: '구경(mm)' } }, + { name: 'wpCpcty2', fieldName: 'wpCpcty2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo2', fieldName: 'pumpCo2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo2', fieldName: 'pumpPreparCo2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpCalbr3', fieldName: 'pumpCalbr3', header: { text: '구경(mm)' } }, + { name: 'wpCpcty3', fieldName: 'wpCpcty3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo3', fieldName: 'pumpCo3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo3', fieldName: 'pumpPreparCo3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + /* 4. 부대시설 및 방식 */ + { name: 'freFnnc', fieldName: 'freFnnc', header: { text: '전처리시설(유/무)' } }, + { name: 'dorzeqpFnnc', fieldName: 'dorzeqpFnnc', header: { text: '탈취설비(유/무)' } }, + { name: 'selfEtrqFnnc', fieldName: 'selfEtrqFnnc', header: { text: '자가발전(유/무)' } }, + { name: 'draMth', fieldName: 'draMth', header: { text: '배수방식' } }, + { name: 'elecMth', fieldName: 'elecMth', header: { text: '저류방식' } }, + { name: 'elecProMat', fieldName: 'elecProMat', header: { text: '저류수 처리방법' } }, + { name: 'disWat', fieldName: 'disWat', header: { text: '방류수역' } }, + + /* 5. 최종처분량 및 방법 */ + { name: 'sdmtTreCpcty', fieldName: 'sdmtTreCpcty', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'iprtTreCpcty', fieldName: 'iprtTreCpcty', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'sdmtTreMth', fieldName: 'sdmtTreMth', header: { text: '침사' } }, + { name: 'iprtTreMth', fieldName: 'iprtTreMth', header: { text: '협잡물' } }, + + /* 6. 운영실적 */ + { name: 'yrstpRnDyn', fieldName: 'yrstpRnDyn', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'dsplcmntAmnt', fieldName: 'dsplcmntAmnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ { - name: 'disposalAmtGroup', - header: { text: '최종처분량(ton/년)' }, - items: ['SDMT_TRE_CPCTY', 'IPRT_TRE_CPCTY'] + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', 'swgeStrgeNm', 'addr' + ] }, + 'displayYear', 'rnDt', 'plDsgnFrqnc', + { name: 'purposeGroup', header: { text: '설치목적' }, items: ['insPer1', 'insPer2', 'insPer3'] }, + 'facRes', 'dgrAr', 'swgeSgbsCpcty', { - name: 'disposalMthGroup', - header: { text: '최종처분방법' }, - items: ['SDMT_TRE_MTH', 'IPRT_TRE_MTH'] + name: 'pumpMainGroup', header: { text: '펌프시설' }, + items: [ + 'totCpcty', + { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpCalbr', 'wpCpcty', 'pumpCo', 'pumpPreparCo'] }, + { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpCalbr2', 'wpCpcty2', 'pumpCo2', 'pumpPreparCo2'] }, + { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpCalbr3', 'wpCpcty3', 'pumpCo3', 'pumpPreparCo3'] } + ] }, - { - name: 'opGroup', - header: { text: '운영실적' }, - items: ['YRSTP_RN_DYN', 'DSPLCMNT_AMNT'] - }, - 'RMK' + { name: 'subFacGroup', header: { text: '부대시설' }, items: ['freFnnc', 'dorzeqpFnnc', 'selfEtrqFnnc'] }, + 'draMth', 'elecMth', 'elecProMat', 'disWat', + { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['sdmtTreCpcty', 'iprtTreCpcty'] }, + { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['sdmtTreMth', 'iprtTreMth'] }, + { name: 'opGroup', header: { text: '운영실적' }, items: ['yrstpRnDyn', 'dsplcmntAmnt'] }, + 'rmk' ] }, - { +{ tabKey: 'b', tabLabel: '빗물펌프장', rowStartNum: 3, fixedColumns: 2, columns: [ /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'SWGE_STRGE_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'ADDR', fieldName: '위치', header: { text: '위치' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 2. 시설 제원 */ - { name: 'FCT_KND', fieldName: '시설물종류', header: { text: '시설물종류' } }, - { name: 'RN_DT', fieldName: '가동개시 년월일', header: { text: '가동개시\n년월일' } }, - { name: 'PL_DSGN_FRQNC', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } }, - { name: 'DGR_AR', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - - /* 3. 유수지 및 흡수정 */ - { name: 'REV_FNNC', fieldName: '유수지유무(유,무)', header: { text: '유수지유무\n(유,무)' } }, - { name: 'REV_CPCTY', fieldName: '저류량(유수지)', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ABS_CPCTY', fieldName: '저류량(흡수정)', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_SGBS_CPCTY', fieldName: '시설용량(㎥)', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 4. 펌프시설 */ - { name: 'TOT_CPCTY', fieldName: '총양수량(㎥/min)', header: { text: '총양수량\n(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CALBR', fieldName: '구경(mm)_1', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY', fieldName: '대당 양수량_1', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO', fieldName: '펌프대수_1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO', fieldName: '예비펌프대수_1', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_CALBR2', fieldName: '구경(mm)_2', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY2', fieldName: '대당 양수량_2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO2', fieldName: '펌프대수_2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO2', fieldName: '예비펌프대수_2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_CALBR3', fieldName: '구경(mm)_3', header: { text: '구경(mm)' } }, - { name: 'WP_CPCTY3', fieldName: '대당 양수량_3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_CO3', fieldName: '펌프대수_3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_PREPAR_CO3', fieldName: '예비펌프대수_3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 5. 부대시설 및 기타 */ - { name: 'FRE_FNNC', fieldName: '전처리시설', header: { text: '전처리시설' } }, - { name: 'DORZEQP_FNNC', fieldName: '탈취설비', header: { text: '탈취설비' } }, - { name: 'SELF_ETRQ_FNNC', fieldName: '자가발전', header: { text: '자가발전' } }, - { name: 'DIS_WAT', fieldName: '방류수역', header: { text: '방류수역' } }, - - /* 6. 최종처분량 및 방법 */ - { name: 'SDMT_TRE_CPCTY', fieldName: '침사(량)', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'IPRT_TRE_CPCTY', fieldName: '협잡물(량)', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SDMT_TRE_MTH', fieldName: '침사(방법)', header: { text: '침사(방법)' } }, - { name: 'IPRT_TRE_MTH', fieldName: '협잡물(방법)', header: { text: '협잡물(방법)' } }, - - /* 7. 운영실적 */ - { name: 'YRSTP_RN_DYN', fieldName: '가동일수', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DSPLCMNT_AMNT', fieldName: '총배수량(㎥/년)', header: { text: '총배수량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { + 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); + 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)); + } + } + 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: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true, editable: false }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true, 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: 'fctKnd', fieldName: 'fctKnd', header: { text: '시설물종류' } }, + { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동개시 년월일' } }, + { name: 'plDsgnFrqnc', fieldName: 'plDsgnFrqnc', header: { text: '계획설계빈도' } }, + { name: 'dgrAr', fieldName: 'dgrAr', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, + { name: 'revFnnc', fieldName: 'revFnnc', header: { text: '유수지유무\n(유,무)' } }, + { name: 'revCpcty', fieldName: 'revCpcty', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'absCpcty', fieldName: 'absCpcty', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'swgeSgbsCpcty', fieldName: 'swgeSgbsCpcty', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'totCpcty', fieldName: 'totCpcty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCalbr', fieldName: 'pumpCalbr', header: { text: '구경(mm)' } }, + { name: 'wpCpcty', fieldName: 'wpCpcty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo', fieldName: 'pumpCo', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo', fieldName: 'pumpPreparCo', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpCalbr2', fieldName: 'pumpCalbr2', header: { text: '구경(mm)' } }, + { name: 'wpCpcty2', fieldName: 'wpCpcty2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo2', fieldName: 'pumpCo2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo2', fieldName: 'pumpPreparCo2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpCalbr3', fieldName: 'pumpCalbr3', header: { text: '구경(mm)' } }, + { name: 'wpCpcty3', fieldName: 'wpCpcty3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpCo3', fieldName: 'pumpCo3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpPreparCo3', fieldName: 'pumpPreparCo3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'freFnnc', fieldName: 'freFnnc', header: { text: '전처리시설' } }, + { name: 'dorzeqpFnnc', fieldName: 'dorzeqpFnnc', header: { text: '탈취설비' } }, + { name: 'selfEtrqFnnc', fieldName: 'selfEtrqFnnc', header: { text: '자가발전' } }, + { name: 'disWat', fieldName: 'disWat', header: { text: '방류수역' } }, + { name: 'sdmtTreCpcty', fieldName: 'sdmtTreCpcty', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'iprtTreCpcty', fieldName: 'iprtTreCpcty', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'sdmtTreMth', fieldName: 'sdmtTreMth', header: { text: '침사(방법)' } }, + { name: 'iprtTreMth', fieldName: 'iprtTreMth', header: { text: '협잡물(방법)' } }, + { name: 'yrstpRnDyn', fieldName: 'yrstpRnDyn', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'dsplcmntAmnt', fieldName: 'dsplcmntAmnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SWGE_STRGE_NM', 'ADDR', 'YR_GBN', 'FCT_KND', 'RN_DT', 'PL_DSGN_FRQNC', 'DGR_AR', - { name: 'revGroup', header: { text: '유수지' }, items: ['REV_FNNC', 'REV_CPCTY'] }, - { name: 'absGroup', header: { text: '흡수정' }, items: ['ABS_CPCTY'] }, - 'SWGE_SGBS_CPCTY', + { + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', 'swgeStrgeNm', 'addr' + ] + }, + 'displayYear', 'fctKnd', 'rnDt', 'plDsgnFrqnc', 'dgrAr', + { name: 'revGroup', header: { text: '유수지' }, items: ['revFnnc', 'revCpcty'] }, + { name: 'absGroup', header: { text: '흡수정' }, items: ['absCpcty'] }, + 'swgeSgbsCpcty', { name: 'pumpMainGroup', header: { text: '펌프시설' }, items: [ - 'TOT_CPCTY', - { name: 'p1Group', header: { text: '펌프시설1' }, items: ['PUMP_CALBR', 'WP_CPCTY', 'PUMP_CO', 'PUMP_PREPAR_CO'] }, - { name: 'p2Group', header: { text: '펌프시설2' }, items: ['PUMP_CALBR2', 'WP_CPCTY2', 'PUMP_CO2', 'PUMP_PREPAR_CO2'] }, - { name: 'p3Group', header: { text: '펌프시설3' }, items: ['PUMP_CALBR3', 'WP_CPCTY3', 'PUMP_CO3', 'PUMP_PREPAR_CO3'] } + 'totCpcty', + { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpCalbr', 'wpCpcty', 'pumpCo', 'pumpPreparCo'] }, + { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpCalbr2', 'wpCpcty2', 'pumpCo2', 'pumpPreparCo2'] }, + { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpCalbr3', 'wpCpcty3', 'pumpCo3', 'pumpPreparCo3'] } ] }, - { name: 'subFacGroup', header: { text: '부대시설' }, items: ['FRE_FNNC', 'DORZEQP_FNNC', 'SELF_ETRQ_FNNC'] }, - 'DIS_WAT', - { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['SDMT_TRE_CPCTY', 'IPRT_TRE_CPCTY'] }, - { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['SDMT_TRE_MTH', 'IPRT_TRE_MTH'] }, - { name: 'opGroup', header: { text: '운영실적' }, items: ['YRSTP_RN_DYN', 'DSPLCMNT_AMNT'] }, - 'RMK' + { name: 'subFacGroup', header: { text: '부대시설' }, items: ['freFnnc', 'dorzeqpFnnc', 'selfEtrqFnnc'] }, + 'disWat', + { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['sdmtTreCpcty', 'iprtTreCpcty'] }, + { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['sdmtTreMth', 'iprtTreMth'] }, + { name: 'opGroup', header: { text: '운영실적' }, items: ['yrstpRnDyn', 'dsplcmntAmnt'] }, + '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 4702636a8..20e3b532f 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -19,6 +19,11 @@ import { type SewerPipeMtcApiResponse, type SewerPipeDrdgApiResponse } from "../model/statistics-sewage-pipe-mtc-review-interface"; +import { + type SewerReservoirRegionalApiResponse, + type PumpingStationRegionalApiResponse +} from "../model/sewage-reservoir-pump-review-interface"; + export class StatisticsManagementApiService { @@ -174,6 +179,44 @@ public async getSewerPipeDrdgList( ); return response.data; } + +/** + * 04. 하수저류시설 tab1. 시설현황 조회 + * @param params (yr, bsnmCd, sido, gugun) + */ +public async getSewerReservoirList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } +): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/facilities`, + { params } + ); + return response.data; +} + +/** + * 04. 빗물펌프장 tab2. 조회 + * @param params (yr, bsnmCd, sido, gugun) + */ +public async getPumpStationList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } +): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/pumping-stations`, + { params } + ); + return response.data; +} } // 통합 인스턴스 수출 diff --git a/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts b/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts new file mode 100644 index 000000000..4f7b6bf7f --- /dev/null +++ b/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts @@ -0,0 +1,171 @@ + +import type { ApiResponse } from "./statistics-interface"; + +/** + * 04. 하수저류시설 tab1. 시설현황 아이템 인터페이스 (Tab1) + */ +export interface SewerReservoirRegionalItem { +// 1. 기본 정보 및 행 제어 + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + swgeStrgeNm: string; // 시설명 + addr: string; // 위치(주소) + yr: string; // 연도 + rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 + displayYear: string; // 구분 표시용 (2024년, 2023년확정 등) + + // 2. 시설 제원 + rnDt: string; // 가동개시일 (yyyyMMdd) + plDsgnFrqnc: string; // 계획설계빈도 + insPer1: string; // 설치목적: 침수예방 + insPer2: string; // 설치목적: 수질오염저감 + insPer3: string; // 설치목적: 하수재이용 + facRes: string; // 시설구조 + dgrAr: number; // 집수구역면적 + swgeSgbsCpcty: number; // 시설용량 + totCpcty: number; // 총양수량 + + // 3. 펌프시설 상세 (1호기) + pumpCalbr: string; // 구경 + wpCpcty: number; // 대당 양수량 + pumpCo: number; // 펌프대수 + pumpPreparCo: number; // 예비펌프대수 + + // 4. 펌프시설 상세 (2호기) - 추가 + pumpCalbr2: string; // 구경2 + wpCpcty2: number; // 대당 양수량2 + pumpCo2: number; // 펌프대수2 + pumpPreparCo2: number; // 예비펌프대수2 + + // 5. 펌프시설 상세 (3호기) - 추가 + pumpCalbr3: string; // 구경3 + wpCpcty3: number; // 대당 양수량3 + pumpCo3: number; // 펌프대수3 + pumpPreparCo3: number; // 예비펌프대수3 + + // 6. 부대시설 및 방식 - 추가 + freFnnc: string; // 전처리시설유무 + dorzeqpFnnc: string; // 탈취설비유무 + selfEtrqFnnc: string; // 자가발전유무 + draMth: string; // 배수방식 + elecMth: string; // 저류방식 + elecProMat: string; // 저류수 처리방법 + disWat: string; // 방류수역 + + // 7. 최종처분량 및 방법 - 추가 + sdmtTreCpcty: number; // 침사량 + iprtTreCpcty: number; // 협잡물량 + sdmtTreMth: string; // 침사 처분방법 + iprtTreMth: string; // 협잡물 처분방법 + + // 8. 운영 실적 + yrstpRnDyn: number; // 가동일수 + dsplcmntAmnt: number; // 총배수량 + rmk: string; // 비고 + + // 9. 상태 체크용 (그리드 배경색 제어용) - 추가 + lcnt: number; // 신규 여부 (1/0) + ocnt: number; // 기존 여부 (1/0) +} + +/** + * 하수저류시설 응답 데이터 구조 + */ +export interface SewerReservoirResponseData { + reservoirList: SewerReservoirRegionalItem[]; +} + +/** + * 하수저류시설 서비스 인터페이스 + */ +export interface SewerReservoirService { + /** + * 하수저류시설 리스트 조회 (Tab1) + */ + getSewerReservoirList(params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업장 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + }): Promise; +} + +/** + * 04. 빗물펌프장 tab2. 시설현황 아이템 인터페이스 (Tab2) + */ +export interface PumpingStationRegionalItem { + // --- 기본 정보 --- + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + swgeStrgeNm: string; // 시설명 + addr: string; // 위치 + yr: string; // 연도 + rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 + displayYear: string; // 구분 표시용 + + // --- 시설 제원 --- + fctKnd: string; // 시설물종류 + rnDt: string; // 가동개시일 + plDsgnFrqnc: string; // 계획설계빈도 + dgrAr: number; // 집수구역면적 + revFnnc: string; // 유수지유무 + revCpcty: number; // 유수지저류량 + absCpcty: number; // 흡수정저류량 + swgeSgbsCpcty: number; // 시설용량 + totCpcty: number; // 총양수량 + + // --- 펌프시설 상세 (1, 2, 3호기) --- + pumpCalbr: string; wpCpcty: number; pumpCo: number; pumpPreparCo: number; + pumpCalbr2: string; wpCpcty2: number; pumpCo2: number; pumpPreparCo2: number; + pumpCalbr3: string; wpCpcty3: number; pumpCo3: number; pumpPreparCo3: number; + + // --- 부대시설 및 방식 --- + freFnnc: string; // 전처리시설유무 + dorzeqpFnnc: string; // 탈취설비유무 + selfEtrqFnnc: string; // 자가발전유무 + disWat: string; // 방류수역 + + // --- 최종처분량 및 방법 --- + sdmtTreCpcty: number; // 침사 처분량 + iprtTreCpcty: number; // 협잡물 처분량 + sdmtTreMth: string; // 침사 처분방법 + iprtTreMth: string; // 협잡물 처분방법 + + // --- 운영실적 --- + yrstpRnDyn: number; // 가동일수 + dsplcmntAmnt: number; // 총배수량 + rmk: string; // 비고 +} + +/** + * 응답 타입 정의 + */ +export type SewerReservoirRegionalApiResponse = ApiResponse; +export type PumpingStationRegionalApiResponse = ApiResponse; + +/** + * 하수저류시설 및 빗물펌프장 서비스 인터페이스 + */ +export interface SewerReservoirService { + /** + * 하수저류시설 리스트 조회 (Tab1) + */ + getSewerReservoirList(params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + }): Promise; + + /** + * 빗물펌프장 리스트 조회 (Tab2) + */ + getPumpStationList(params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + }): Promise; +} \ No newline at end of file From b6a75f6ee02899b5cd2846b5280c81d03df14f98 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 20 Feb 2026 14:02:27 +0900 Subject: [PATCH 006/113] =?UTF-8?q?feat:=2005.=ED=8E=8C=ED=94=84=EC=9E=A5?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/PumpingStationRvw.tsx | 98 +++++++-- .../column/review/PumpingStationRvwColumn.ts | 206 +++++++++++++----- .../api/statistics-management-service.ts | 24 +- ...istics-pumping-station-review-interface.ts | 86 ++++++++ 4 files changed, 343 insertions(+), 71 deletions(-) create mode 100644 apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts diff --git a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx index 8a9df541a..b1a1d6f9d 100644 --- a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx +++ b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx @@ -1,26 +1,82 @@ -import { useState } from "react"; - -import { DataGrid } from "@rsone/components"; +import { useState, useMemo } from "react"; +import DataGrid from "@/components/module/DataGrid"; 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 { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; -// 하수도 통계 관리 05.펌프장 +// 하수도 통계 검토 05.중계펌프장 const PumpingStationRvw = () => { - const [rowData, setRowData] = useState[]>([]); + const { setMessage: alert } = useAlertStore((state) => state); + const [rowData, setRowData] = useState([]); + const [isLoading, setIsLoading] = useState(false); - const approveFunc = () => { console.log('approveFunc'); }; - const saveFunc = () => { console.log('submit'); }; - const reviewOpinionFunc = () => { console.log('reviewOpinionFunc'); }; - const returnFunc = () => { console.log('returnFunc'); }; + const columnInfo = useMemo(() => { + const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList; + return { + columnLayout, + fixedColumns, + rowStartNum, + columns: columns.map((col) => ({ + ...col, + editable: false as const, // 검토 화면이므로 읽기 전용 + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + }; + }, []); + + const handleReset = () => { + setRowData([]); + }; + + const handleSearch = async (values: SearchFormValues) => { + if (values.sido === '' || values.sido === 'ALL') { + alert('시도를 선택해주세요.'); + return; + } + + try { + setIsLoading(true); + + const response = await statisticsManagementApi.getMediationPumpStationList({ + sido: values.sido, + gugun: values.gugun === 'ALL' ? '' : values.gugun, + bsnmCd: values.bsnmCd, + yr: values.year, + bscFctNm: values.fctName, // 시설명 검색 + }); + + if (response.status === 'success' && response.data) { + setRowData(toGridRows(response.data)); + } else { + setRowData([]); + alert(response.message || "데이터가 없습니다."); + } + } catch (error) { + console.error("중계펌프장 조회 중 에러 발생:", error); + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const approveFunc = () => console.log('approveFunc'); + const saveFunc = () => console.log('submit'); + const reviewOpinionFunc = () => console.log('reviewOpinionFunc'); + const returnFunc = () => console.log('returnFunc'); return ( - <> - +
+ +
@@ -29,8 +85,24 @@ const PumpingStationRvw = () => {
- - + + {isLoading && ( +
+
+ +
+
+ )} + + +
); }; diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index 058eb39e3..7ab5e8e59 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -1,106 +1,198 @@ import { ValueType } from "realgrid"; +import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; +import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; -// 하수도 통계 관리 05.펌프장 export const rawPumpingStationInfoList = { columns: [ /* 1. 행정구역 및 기본정보 */ - { name: 'sido', fieldName: '시도', header: { text: '시도' } }, - { name: 'sgg', fieldName: '구군', header: { text: '구군' } }, - { name: 'PUMP_NM', fieldName: '펌프장명', header: { text: '펌프장명' } }, - { name: 'GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TRT_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' } }, - { name: 'TYPE_GBN', fieldName: '구분(합류식분류식)', header: { text: '구분(합류식분류식)' } }, - { name: 'ADDR', fieldName: '위치', header: { text: '위치' } }, - { name: 'RN_DT', fieldName: '가동년월일 yyyy-mm-dd', header: { text: '가동년월일\nyyyy-mm-dd' } }, - { name: 'DSN_AR', fieldName: '계획배수면적(km2)', header: { text: '계획배수면적\n(km2)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { + name: 'sido', + fieldName: 'sido', + header: { text: '시도' }, + mergeCells: true, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + } + ] + }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'pumpNm', fieldName: 'pumpNm', header: { text: '펌프장명' }, mergeCells: true, editable: false, sortdisable: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + editable: false, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + } + ] + }, + { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '처리장명' } }, + { name: 'pumpCls', fieldName: 'pumpCls', header: { text: '구분\n(합류/분류)' } }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' }, styleName: 'rg-cell-left' }, + { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동년월일\nyyyy-mm-dd' } }, + { + name: 'planDrhgAr', fieldName: 'planDrhgAr', header: { text: '계획배수면적\n(km2)' }, + footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' + }, /* 2. 계획배수량 (m3/hr) */ - { name: 'DSN_QTY_A', fieldName: '청천시(A)', header: { text: '청천시(A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSN_QTY_B', fieldName: '우천시(B)', header: { text: '우천시(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSN_QTY_DIFF', fieldName: '우수량(B-A)', header: { text: '우수량(B-A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'finewtDrngVl', fieldName: 'finewtDrngVl', header: { text: '청천시(A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtDrngVl', fieldName: 'rainwtDrngVl', header: { text: '우천시(B)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'vlWtr', fieldName: 'vlWtr', header: { text: '우수량(B-A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, /* 3. 총 양수량 (m3/min) */ - { name: 'TOT_PUMP_QTY_O', fieldName: '오수펌프 총양수량', header: { text: '오수펌프\n총양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'TOT_PUMP_QTY_W', fieldName: '우수펌프 총양수량', header: { text: '우수펌프\n총양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrTotCpcty', fieldName: 'pltwtrTotCpcty', header: { text: '오수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrTotCpcty', fieldName: 'rainwtrTotCpcty', header: { text: '우수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, /* 4. 오수펌프 상세 (1, 2, 3) */ - { name: 'PUMP_O1_D', fieldName: '구경(mm)_오수1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O1_Q', fieldName: '양수량(m3/min)_오수1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O1_C', fieldName: '펌프대수_오수1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_O1_S', fieldName: '예비대수_오수1', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrPumpCalbr1', fieldName: 'pltwtrPumpCalbr1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCpcty1', fieldName: 'pltwtrPumpCpcty1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCnt1', fieldName: 'pltwtrPumpCnt1', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrPumpPreparCnt1', fieldName: 'pltwtrPumpPreparCnt1', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + { name: 'pltwtrPumpCalbr2', fieldName: 'pltwtrPumpCalbr2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCpcty2', fieldName: 'pltwtrPumpCpcty2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCnt2', fieldName: 'pltwtrPumpCnt2', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrPumpPreparCnt2', fieldName: 'pltwtrPumpPreparCnt2', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_O2_D', fieldName: '구경(mm)_오수2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O2_Q', fieldName: '양수량(m3/min)_오수2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O2_C', fieldName: '펌프대수_오수2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_O2_S', fieldName: '예비대수_오수2', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - { name: 'PUMP_O3_D', fieldName: '구경(mm)_오수3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O3_Q', fieldName: '양수량(m3/min)_오수3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_O3_C', fieldName: '펌프대수_오수3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_O3_S', fieldName: '예비대수_오수3', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrPumpCalbr3', fieldName: 'pltwtrPumpCalbr3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCpcty3', fieldName: 'pltwtrPumpCpcty3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'pltwtrPumpCnt3', fieldName: 'pltwtrPumpCnt3', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrPumpPreparCnt3', fieldName: 'pltwtrPumpPreparCnt3', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, /* 5. 우수펌프 상세 (1, 2, 3) */ - { name: 'PUMP_W1_D', fieldName: '구경(mm)_우수1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W1_Q', fieldName: '양수량(m3/min)_우수1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W1_C', fieldName: '펌프대수_우수1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_W1_S', fieldName: '예비대수_우수1', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpCalbr1', fieldName: 'rainwtrPumpCalbr1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCpcty1', fieldName: 'rainwtrPumpCpcty1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCnt1', fieldName: 'rainwtrPumpCnt1', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpPreparCnt1', fieldName: 'rainwtrPumpPreparCnt1', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_W2_D', fieldName: '구경(mm)_우수2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W2_Q', fieldName: '양수량(m3/min)_우수2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W2_C', fieldName: '펌프대수_우수2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_W2_S', fieldName: '예비대수_우수2', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpCalbr2', fieldName: 'rainwtrPumpCalbr2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCpcty2', fieldName: 'rainwtrPumpCpcty2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCnt2', fieldName: 'rainwtrPumpCnt2', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpPreparCnt2', fieldName: 'rainwtrPumpPreparCnt2', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_W3_D', fieldName: '구경(mm)_우수3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W3_Q', fieldName: '양수량(m3/min)_우수3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PUMP_W3_C', fieldName: '펌프대수_우수3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PUMP_W3_S', fieldName: '예비대수_우수3', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpCalbr3', fieldName: 'rainwtrPumpCalbr3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCpcty3', fieldName: 'rainwtrPumpCpcty3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rainwtrPumpCnt3', fieldName: 'rainwtrPumpCnt3', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'rainwtrPumpPreparCnt3', fieldName: 'rainwtrPumpPreparCnt3', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 6. 운영 정보 */ - { name: 'PWR_USAGE', fieldName: '연간전력사용량(kWh/년)', header: { text: '연간전력사용량\n(kWh/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + /* 6. 기타 및 운영 */ + { name: 'elcpwAmnt', fieldName: 'elcpwAmnt', header: { text: '연간전력사용량\n(kWh/년)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, ], columnLayout: [ - { name: 'groupA', header: { text: '행정구역' }, items: ['sido', 'sgg'] }, - 'PUMP_NM', 'GBN', 'TRT_FCT_NM', 'TYPE_GBN', 'ADDR', 'RN_DT', 'DSN_AR', + { + name: 'groupA', + header: { text: '행정구역' }, + items: [ + { column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + 'gugun', + 'pumpNm', + ] + }, + 'displayYear', + 'bscFctNm', + 'pumpCls', + 'addr', + 'rnDt', + 'planDrhgAr', { name: 'groupB', header: { text: '계획시간대최대배수량(m3/hr)' }, - items: ['DSN_QTY_A', 'DSN_QTY_B', 'DSN_QTY_DIFF'], + items: ['finewtDrngVl', 'rainwtDrngVl', 'vlWtr'], }, { name: 'groupC', header: { text: '배수능력' }, items: [ - 'TOT_PUMP_QTY_O', + 'pltwtrTotCpcty', { name: 'pumpO_Detail', header: { text: '오수펌프 상세' }, items: [ - { name: 'O1', header: { text: '오수펌프(1)' }, items: ['PUMP_O1_D', 'PUMP_O1_Q', 'PUMP_O1_C', 'PUMP_O1_S'] }, - { name: 'O2', header: { text: '오수펌프(2)' }, items: ['PUMP_O2_D', 'PUMP_O2_Q', 'PUMP_O2_C', 'PUMP_O2_S'] }, - { name: 'O3', header: { text: '오수펌프(3)' }, items: ['PUMP_O3_D', 'PUMP_O3_Q', 'PUMP_O3_C', 'PUMP_O3_S'] } + { name: 'O1', header: { text: '오수펌프(1)' }, items: ['pltwtrPumpCalbr1', 'pltwtrPumpCpcty1', 'pltwtrPumpCnt1', 'pltwtrPumpPreparCnt1'] }, + { name: 'O2', header: { text: '오수펌프(2)' }, items: ['pltwtrPumpCalbr2', 'pltwtrPumpCpcty2', 'pltwtrPumpCnt2', 'pltwtrPumpPreparCnt2'] }, + { name: 'O3', header: { text: '오수펌프(3)' }, items: ['pltwtrPumpCalbr3', 'pltwtrPumpCpcty3', 'pltwtrPumpCnt3', 'pltwtrPumpPreparCnt3'] } ] }, - 'TOT_PUMP_QTY_W', + 'rainwtrTotCpcty', { name: 'pumpW_Detail', header: { text: '우수펌프 상세' }, items: [ - { name: 'W1', header: { text: '우수펌프(1)' }, items: ['PUMP_W1_D', 'PUMP_W1_Q', 'PUMP_W1_C', 'PUMP_W1_S'] }, - { name: 'W2', header: { text: '우수펌프(2)' }, items: ['PUMP_W2_D', 'PUMP_W2_Q', 'PUMP_W2_C', 'PUMP_W2_S'] }, - { name: 'W3', header: { text: '우수펌프(3)' }, items: ['PUMP_W3_D', 'PUMP_W3_Q', 'PUMP_W3_C', 'PUMP_W3_S'] } + { name: 'W1', header: { text: '우수펌프(1)' }, items: ['rainwtrPumpCalbr1', 'rainwtrPumpCpcty1', 'rainwtrPumpCnt1', 'rainwtrPumpPreparCnt1'] }, + { name: 'W2', header: { text: '우수펌프(2)' }, items: ['rainwtrPumpCalbr2', 'rainwtrPumpCpcty2', 'rainwtrPumpCnt2', 'rainwtrPumpPreparCnt2'] }, + { name: 'W3', header: { text: '우수펌프(3)' }, items: ['rainwtrPumpCalbr3', 'rainwtrPumpCpcty3', 'rainwtrPumpCnt3', 'rainwtrPumpPreparCnt3'] } ] } ], }, - 'PWR_USAGE', 'RMK', - ] + 'elcpwAmnt', + 'rmk', + ], + fixedColumns: 2, + rowStartNum: 3 }; export const PumpingStationInfoList = { ...rawPumpingStationInfoList, columns: rawPumpingStationInfoList.columns.map(col => ({ ...col, - editable: false + editable: false, + styleName: col.styleName ?? "rg-cell-readonly" })) }; \ 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 20e3b532f..9fae80f45 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -23,7 +23,9 @@ import { type SewerReservoirRegionalApiResponse, type PumpingStationRegionalApiResponse } from "../model/sewage-reservoir-pump-review-interface"; - +import { + type MediationPumpStationApiResponse, +} from "../model/statistics-pumping-station-review-interface"; export class StatisticsManagementApiService { @@ -217,6 +219,26 @@ public async getPumpStationList( ); return response.data; } + +/** + * 05. 중계펌프장 통계관리 조회 + * @param params (yr, bsnmCd, sido, gugun, bscFctNm) + */ +public async getMediationPumpStationList( + params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업소 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + bscFctNm?: string; // 펌프장명 (시설명) + } +): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/mediation-pump-stations`, + { params } + ); + return response.data; +} } // 통합 인스턴스 수출 diff --git a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts new file mode 100644 index 000000000..fbfc04711 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts @@ -0,0 +1,86 @@ +import type { ApiResponse } from "./statistics-interface"; + +/** + * 중계펌프장 통계관리 아이템 인터페이스 + */ +export interface MediationPumpStationItem { + // 기본 정보 및 행 제어 + sido: string; // 시도 + gugun: string; // 시군구 + adstCd: string; // 행정코드 + pumpNm: string; // 펌프장명 + yr: string; // 연도 + displayYear: string; // 구분 표시용 (2024년확정 등) + rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 + + // 주요 제원 + bscFctNm: string; // 처리장명 + pumpCls: string; // 구분 (합류식/분류식) + addr: string; // 위치 + rnDt: string; // 가동년월일 + planDrhgAr: number; // 계획배수면적(㎢) + finewtDrngVl: number; // 계획시간대최대배수량 - 청천시(A) + rainwtDrngVl: number; // 계획시간대최대배수량 - 우천시(B) + vlWtr: number; // 계획시간대최대배수량 - 우수량(B-A) + + // 오수펌프 상세 (PLTWTR) + pltwtrTotCpcty: number; // 오수 총양수량(㎥/min) + pltwtrPumpCalbr1: number; // 오수펌프1 구경(mm) + pltwtrPumpCpcty1: number; // 오수펌프1 양수량(㎥/min) + pltwtrPumpCnt1: number; // 오수펌프1 펌프대수 + pltwtrPumpPreparCnt1: number; // 오수펌프1 예비대수 + + pltwtrPumpCalbr2: number; // 오수펌프2 구경(mm) + pltwtrPumpCpcty2: number; // 오수펌프2 양수량(㎥/min) + pltwtrPumpCnt2: number; // 오수펌프2 펌프대수 + pltwtrPumpPreparCnt2: number; // 오수펌프2 예비대수 + + pltwtrPumpCalbr3: number; // 오수펌프3 구경(mm) + pltwtrPumpCpcty3: number; // 오수펌프3 양수량(㎥/min) + pltwtrPumpCnt3: number; // 오수펌프3 펌프대수 + pltwtrPumpPreparCnt3: number; // 오수펌프3 예비대수 + + // 우수펌프 상세 (RAINWTR) + rainwtrTotCpcty: number; // 우수 총양수량(㎥/min) + rainwtrPumpCalbr1: number; // 우수펌프1 구경(mm) + rainwtrPumpCpcty1: number; // 우수펌프1 양수량(㎥/min) + rainwtrPumpCnt1: number; // 우수펌프1 펌프대수 + rainwtrPumpPreparCnt1: number; // 우수펌프1 예비대수 + + rainwtrPumpCalbr2: number; // 우수펌프2 구경(mm) + rainwtrPumpCpcty2: number; // 우수펌프2 양수량(㎥/min) + rainwtrPumpCnt2: number; // 우수펌프2 펌프대수 + rainwtrPumpPreparCnt2: number; // 우수펌프2 예비대수 + + rainwtrPumpCalbr3: number; // 우수펌프3 구경(mm) + rainwtrPumpCpcty3: number; // 우수펌프3 양수량(㎥/min) + rainwtrPumpCnt3: number; // 우수펌프3 펌프대수 + rainwtrPumpPreparCnt3: number; // 우수펌프3 예비대수 + + // 기타 + elcpwAmnt: number; // 연간전력사용량(kWh/년) + rmk: string; // 비고 + lcnt: string; // 당해 존재 여부 ('1'|'0') + ocnt: string; // 전년 존재 여부 ('1'|'0') +} + + +export interface MediationPumpStationResponseData { + pumpStationList: MediationPumpStationItem[]; +} + + +export type MediationPumpStationApiResponse = ApiResponse & { + message: string; +}; + + +export interface MediationPumpStationService { + getMediationPumpStationList(params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업소 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + bscFctNm?: string; // 펌프장명 + }): Promise; +} \ No newline at end of file From cb29ac6c4d9524f64a2c3b58560510a09c6bbf58 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 20 Feb 2026 15:22:09 +0900 Subject: [PATCH 007/113] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=9F=BC.ts-=20ed?= =?UTF-8?q?itable:=20false,=20styleName:=20"rg-cell-readonly"=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../column/review/PumpingStationRvwColumn.ts | 10 ++- .../review/SewagePipeMaintainRvwColumn.ts | 13 ++-- .../util/column/review/SewagePipeRvwColumn.ts | 27 +++----- .../review/SewageReservoirPumpRvwColumn.ts | 19 +++--- .../SeweragePenetrationRateRvwColumn.ts | 64 +++++++++---------- .../review/StatsInputStatusRvwColumn.ts | 3 +- 6 files changed, 58 insertions(+), 78 deletions(-) diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index 7ab5e8e59..f031fed5e 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -10,7 +10,6 @@ export const rawPumpingStationInfoList = { fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -43,13 +42,12 @@ export const rawPumpingStationInfoList = { } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'pumpNm', fieldName: 'pumpNm', header: { text: '펌프장명' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'pumpNm', fieldName: 'pumpNm', header: { text: '펌프장명' }, mergeCells: true, sortdisable: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -82,7 +80,7 @@ export const rawPumpingStationInfoList = { }, { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '처리장명' } }, { name: 'pumpCls', fieldName: 'pumpCls', header: { text: '구분\n(합류/분류)' } }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' }, styleName: 'rg-cell-left' }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' } }, { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동년월일\nyyyy-mm-dd' } }, { name: 'planDrhgAr', fieldName: 'planDrhgAr', header: { text: '계획배수면적\n(km2)' }, @@ -193,6 +191,6 @@ export const PumpingStationInfoList = { columns: rawPumpingStationInfoList.columns.map(col => ({ ...col, editable: false, - styleName: col.styleName ?? "rg-cell-readonly" + styleName: "rg-cell-readonly" })) }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index ab32f39a5..f0ca4e7bc 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -16,8 +16,7 @@ const rawMaintainList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, - sortdisable: true, + tdisable: true, footer: [ { valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { @@ -53,14 +52,13 @@ const rawMaintainList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, /* 2. 구분 (displayYear) */ { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -165,7 +163,6 @@ const rawMaintainList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -198,12 +195,11 @@ const rawMaintainList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -291,6 +287,7 @@ export const SewagePipeMaintainColumnInfoList = rawMaintainList.map(tab => ({ ...tab, columns: tab.columns.map(col => ({ ...col, - editable: false + editable: false, + styleName: "rg-cell-readonly" })) })); \ 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 b57a54d44..4cc605836 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -16,7 +16,6 @@ const rawList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -55,13 +54,12 @@ const rawList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true,editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true,sortdisable: true }, + { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -197,7 +195,6 @@ const rawList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -234,13 +231,12 @@ const rawList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' },mergeCells: true, editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' },mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -362,7 +358,6 @@ const rawList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -393,13 +388,12 @@ const rawList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true, editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true,sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -488,7 +482,6 @@ const rawList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -525,13 +518,12 @@ const rawList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true, editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -608,6 +600,7 @@ export const SewagePipeColumnInfoListRvw = rawList.map(tab => ({ ...tab, columns: tab.columns.map(col => ({ ...col, - editable: false + editable: false, + styleName: "rg-cell-readonly" })) })); \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts index b5caf5604..ded536777 100644 --- a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts @@ -21,7 +21,6 @@ const rawReservoirList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -60,14 +59,13 @@ const rawReservoirList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true, editable: false }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true, editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true}, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -189,7 +187,6 @@ const rawReservoirList = [ fieldName: 'sido', header: { text: '시도' }, mergeCells: true, - editable: false, sortdisable: true, footer: [ { @@ -228,14 +225,13 @@ const rawReservoirList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true, editable: false }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true, editable: false }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true }, + { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { @@ -339,6 +335,7 @@ export const SewageReservoirPumpInfoListRvw = rawReservoirList.map(tab => ({ ...tab, columns: tab.columns.map(col => ({ ...col, - editable: false + editable: false, + styleName: "rg-cell-readonly" })) })); \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts b/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts index bf1af2259..fed92fbde 100644 --- a/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts @@ -2,88 +2,75 @@ import { ValueType } from "realgrid"; import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; -export const SeweragePenetrationRateInfoList = { + +const rawSeweragePenetrationRateInfoList = { 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) => { + valueCallback: (grid: GridView) => { 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)); - } + if (divisionValue) uniqueValues.add(String(divisionValue)); } const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { 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)); - } + if (divisionValue) uniqueValues.add(String(divisionValue)); } const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; } } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true }, - { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true, editable: false, sortdisable: true }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, - editable: false, sortdisable: true, footer: [ { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { 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)); - } + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } const divisionValues = Array.from(uniqueValues); return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { 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)); - } + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } const divisionValues = Array.from(uniqueValues); return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; @@ -97,17 +84,15 @@ export const SeweragePenetrationRateInfoList = { { name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' } }, { name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' } }, { name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } }, - { name: 'popltnSm', fieldName: 'popltnSm', header: { text: '총인구' }, footer: footerIntArrayByDivision, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'popltnSm', fieldName: 'popltnSm', header: { text: '총인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'arSm', fieldName: 'arSm', header: { text: '총면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'swgeProcZoneInnerSm', fieldName: 'swgeProcZoneInnerSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-total' }, + { name: 'swgeProcZoneInnerSm', fieldName: 'swgeProcZoneInnerSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - // 공공하수처리인구 { name: 'pblswrgPopltnSm', fieldName: 'pblswrgPopltnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'pblswrgTertTrt', fieldName: 'pblswrgTertTrt', header: { text: '3차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - // 폐수처리인구(명) { name: 'wstrPopltnSm', fieldName: 'wstrPopltnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, @@ -183,4 +168,13 @@ export const SeweragePenetrationRateInfoList = { ], fixedColumns: 2, rowStartNum: 3 +}; + +export const SeweragePenetrationRateInfoList = { + ...rawSeweragePenetrationRateInfoList, + columns: rawSeweragePenetrationRateInfoList.columns.map(col => ({ + ...col, + editable: false, + styleName: "rg-cell-readonly" + })) }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/StatsInputStatusRvwColumn.ts b/apps/st/src/components/util/column/review/StatsInputStatusRvwColumn.ts index a5408cc7f..9f280338c 100644 --- a/apps/st/src/components/util/column/review/StatsInputStatusRvwColumn.ts +++ b/apps/st/src/components/util/column/review/StatsInputStatusRvwColumn.ts @@ -115,6 +115,7 @@ export const StatsInputStatusInfoList = { ...StatsInputStatusInfoColumnRaw, columns: StatsInputStatusInfoColumnRaw.columns.map(col => ({ ...col, - editable: false + editable: false, + styleName: "rg-cell-readonly" })) }; \ No newline at end of file From 8fa00797c8dd4035999c4de4474eed697b7f5f97 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 20 Feb 2026 16:10:39 +0900 Subject: [PATCH 008/113] =?UTF-8?q?refactor:=20tab=20key=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=EB=90=9C=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/SewagePipeMaintainRvw.tsx | 16 +++++++--------- .../statistics/review/SewagePipeRvw.tsx | 16 ++++++++-------- .../statistics/review/SewageReservoirPumpRvw.tsx | 13 +++++-------- .../column/review/SewagePipeMaintainRvwColumn.ts | 4 ++-- .../util/column/review/SewagePipeRvwColumn.ts | 8 ++++---- .../review/SewageReservoirPumpRvwColumn.ts | 4 ++-- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index f2da6cce4..272ae04f1 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -19,7 +19,7 @@ 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 [activeTab, setActiveTab] = useState('ST00030001'); const searchParamRef = useRef(null); @@ -36,7 +36,7 @@ const SewagePipeMaintainRvw = () => { const handleReset = () => { setRowDataMap(new Map()); - setActiveTab('a'); + setActiveTab('ST00030001'); searchParamRef.current = null; }; @@ -46,7 +46,7 @@ const SewagePipeMaintainRvw = () => { return; } - setActiveTab('a'); + setActiveTab('ST00030001'); setRowDataMap(new Map()); try { @@ -65,12 +65,11 @@ const SewagePipeMaintainRvw = () => { 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[])); + newRowDataMap.set('ST00030001', toGridRows(listData as object[])); setRowDataMap(newRowDataMap); } else { alert(response?.message || "데이터가 없습니다."); @@ -85,7 +84,7 @@ const SewagePipeMaintainRvw = () => { const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); - const existingAData = rowDataMap.get('a'); + const existingAData = rowDataMap.get('ST00030001'); const activeTabData = rowDataMap.get(tabKey); const hasAData = Array.isArray(existingAData) && existingAData.length > 0; @@ -97,9 +96,9 @@ const SewagePipeMaintainRvw = () => { let response; - if (tabKey === 'a') { + if (tabKey === 'ST00030001') { response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current); - }else if (tabKey === 'b') { + }else if (tabKey === 'ST00030002') { response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current); } @@ -107,7 +106,6 @@ const SewagePipeMaintainRvw = () => { setRowDataMap(prev => { const next = new Map(prev); - // API 응답이 배열을 직접 반환하는 경우와 객체로 감싸진 경우 모두 처리 const listData = Array.isArray(response.data) ? response.data : (response.data.mtcStatusList || []); diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index ac7033e53..94b79dfb0 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -19,7 +19,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 [activeTab, setActiveTab] = useState('ST00020001'); const searchParamRef = useRef(null); @@ -36,7 +36,7 @@ const SewagePipeRvw = () => { const handleReset = () => { setRowDataMap(new Map()); - setActiveTab('a'); + setActiveTab('ST00020001'); }; const handleSearch = async (values: SearchFormValues) => { @@ -45,7 +45,7 @@ const SewagePipeRvw = () => { return; } - setActiveTab('a'); + setActiveTab('ST00020001'); setRowDataMap(new Map()); @@ -64,7 +64,7 @@ const SewagePipeRvw = () => { if (response?.status === 'success' && response.data) { const newRowDataMap = new Map[]>(); - newRowDataMap.set('a', toGridRows(response.data as object[])); + newRowDataMap.set('ST00020001', toGridRows(response.data as object[])); setRowDataMap(newRowDataMap); } else { alert(response?.message || "데이터가 없습니다."); @@ -79,7 +79,7 @@ const SewagePipeRvw = () => { const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); - const existingAData = rowDataMap.get('a'); + const existingAData = rowDataMap.get('ST00020001'); const activeTabData = rowDataMap.get(tabKey); const hasAData = Array.isArray(existingAData) && existingAData.length > 0; @@ -92,11 +92,11 @@ const SewagePipeRvw = () => { let response; - if (tabKey === 'b') { + if (tabKey === 'ST00020002') { response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current); - }else if(tabKey === 'c'){ + }else if(tabKey === 'ST00020003'){ response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current); - }else if(tabKey === 'd'){ + }else if(tabKey === 'ST00020004'){ response = await statisticsManagementApi.getSewagePipeDiamList(searchParamRef.current); } diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index 3b7645a4e..346edb0cd 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -23,7 +23,7 @@ const SewageReservoirPumpRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState('a'); + const [activeTab, setActiveTab] = useState('ST00040001'); const searchParamRef = useRef(null); @@ -40,18 +40,17 @@ const SewageReservoirPumpRvw = () => { const handleReset = () => { setRowDataMap(new Map()); - setActiveTab('a'); + setActiveTab('ST00040001'); searchParamRef.current = null; }; - // 조회 버튼 클릭 시: Tab A(하수저류시설) 데이터 로드 const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); return; } - setActiveTab('a'); + setActiveTab('ST00040001'); setRowDataMap(new Map()); try { @@ -71,7 +70,7 @@ const SewageReservoirPumpRvw = () => { if (response?.status === 'success' && response.data) { const newRowDataMap = new Map[]>(); - newRowDataMap.set('a', toGridRows(response.data as object[])); + newRowDataMap.set('ST00040001', toGridRows(response.data as object[])); setRowDataMap(newRowDataMap); } else { alert(response?.message || "데이터가 없습니다."); @@ -83,20 +82,18 @@ const SewageReservoirPumpRvw = () => { } }; - // 탭 전환 시: 데이터가 없는 경우 지연 로딩 (Tab B: 빗물펌프장) const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); const activeTabData = rowDataMap.get(tabKey); const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; - // 메인 데이터(Tab A)가 있고, 현재 탭 데이터가 없을 때만 API 호출 if (searchParamRef.current && !hasActiveTabData) { try { setIsLoading(true); let response; - if (tabKey === 'b') { + if (tabKey === 'ST00040002') { response = await statisticsManagementApi.getPumpStationList(searchParamRef.current); } diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index f0ca4e7bc..aaf2bed67 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -5,7 +5,7 @@ import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataG // 하수도 통계 관리 03. 하수관로 유지 관리 const rawMaintainList = [ { - tabKey: 'a', + tabKey: 'ST00030001', tabLabel: '개보수', rowStartNum: 4, fixedColumns: 2, @@ -152,7 +152,7 @@ const rawMaintainList = [ ] }, { - tabKey: 'b', + tabKey: 'ST00030002', tabLabel: '관로준설', rowStartNum: 4, fixedColumns: 2, diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 4cc605836..93f52068f 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -6,7 +6,7 @@ import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataG // 하수도 통계 관리 02. 하수관로 const rawList = [ { - tabKey: 'a', + tabKey: 'ST00020001', tabLabel: '지역별 시설현황', rowStartNum: 4, fixedColumns: 2, @@ -185,7 +185,7 @@ const rawList = [ ] }, { - tabKey: 'b', + tabKey: 'ST00020002', tabLabel: '시설별 설치현황', rowStartNum: 4, fixedColumns: 3, @@ -348,7 +348,7 @@ const rawList = [ ] }, { - tabKey: 'c', + tabKey: 'ST00020003', tabLabel: '관종별 현황', rowStartNum: 2, fixedColumns: 3, @@ -472,7 +472,7 @@ const rawList = [ ] }, { - tabKey: 'd', + tabKey: 'ST00020004', tabLabel: '관경별 현황', rowStartNum: 2, fixedColumns: 3, diff --git a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts index ded536777..415d2896f 100644 --- a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts @@ -10,7 +10,7 @@ import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataG // 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장 const rawReservoirList = [ { - tabKey: 'a', + tabKey: 'ST00040001', tabLabel: '하수저류시설현황', rowStartNum: 3, fixedColumns: 2, @@ -176,7 +176,7 @@ const rawReservoirList = [ ] }, { - tabKey: 'b', + tabKey: 'ST00040002', tabLabel: '빗물펌프장', rowStartNum: 3, fixedColumns: 2, From 103efe79d3197d5d6a9d251c066fd0c2a91b0274 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 23 Feb 2026 10:03:37 +0900 Subject: [PATCH 009/113] =?UTF-8?q?st:=20feat:=20=EC=A7=84=ED=96=89?= =?UTF-8?q?=EC=83=81=ED=99=A9=20=EC=A0=95=EB=B3=B4=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/css/DataGrid.css | 12 + apps/st/src/components/module/DataGrid.tsx | 227 ++++++------------ .../input/SeweragePenetrationRateInp.tsx | 33 ++- apps/st/src/components/type/DataGrid.type.ts | 2 + 4 files changed, 111 insertions(+), 163 deletions(-) diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index f602400c7..888d9fec1 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -21,6 +21,18 @@ @apply !bg-[#e3eaf2]; } + /* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */ + .data-grid-status-bubble { + @apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5; + box-shadow: none; + } + .data-grid-status-bubble span { + @apply text-base text-slate-700; + } + .data-grid-status-bubble span + span { + @apply ml-4 border-l border-slate-200/70 pl-4; + } + [data-theme="black"] { .rg-grid .rg-header .rg-table .rg-header-group-cell { @apply !bg-[#3C4043] !text-[#fff]; diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 7d7f925ee..5eebe138e 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -86,110 +86,6 @@ function toNum(v: unknown): number { return parseFloat(String(v).replace(/,/g, "")) || NaN; } -/** 헤더에 정렬 화살표 표시 (CSS 방식) */ -function updateHeaderSortIndicator( - gridView: { - columnByIndex?: ( - i: number, - ) => { fieldName?: string; name?: string } | undefined; - columnByName?: ( - name: string, - ) => { fieldName?: string; name?: string } | undefined; - getColumn?: ( - i: number, - ) => { fieldName?: string; name?: string } | undefined; - }, - columns: { fieldName?: string; name: string }[], - sortDir: Record, -) { - try { - columns.forEach((col) => { - if (!col.fieldName && !col.name) return; - - // 컬럼 찾기 - let gridCol: { fieldName?: string; name?: string } | undefined; - for (let i = 0; i < 100; i++) { - const c = - gridView.columnByIndex?.(i) ?? gridView.getColumn?.(i); - if (!c) break; - if ( - c.fieldName === (col.fieldName || col.name) || - c.name === (col.fieldName || col.name) - ) { - gridCol = c; - break; - } - } - if (!gridCol) { - gridCol = gridView.columnByName?.(col.name); - } - - if (!gridCol) return; - - const fieldName = - gridCol.fieldName || gridCol.name || col.fieldName || col.name; - const direction = sortDir[fieldName] || sortDir[col.name] || "none"; - - // DOM에서 헤더 요소 찾기 - const gridEl1 = document.querySelector( - "[data-grid-id]", - ) as HTMLElement | null; - const gridEl2 = document.querySelector( - ".rg-grid", - ) as HTMLElement | null; - const gridEl = gridEl1 || gridEl2; - if (!gridEl) return; - - // 헤더 셀 찾기 (fieldName 또는 name으로) - const headerCells = gridEl.querySelectorAll( - '.rg-header-cell, [class*="header"]', - ); - let targetCell: HTMLElement | null = null; - - headerCells.forEach((cell) => { - const el = cell as HTMLElement; - const text = el.textContent?.trim(); - const colName = col.name; - const colFieldName = col.fieldName; - if ( - text && - (text.includes(colName) || - (colFieldName && text.includes(colFieldName))) - ) { - targetCell = el; - } - }); - - if (!targetCell) return; - - const headerCell = targetCell as HTMLElement; - - // 기존 화살표 제거 - const existingIndicator = headerCell.querySelector( - ".rg-sort-indicator", - ) as HTMLElement | null; - if (existingIndicator) { - existingIndicator.remove(); - } - - // 화살표 추가 - if (direction === "asc" || direction === "desc") { - const indicator = document.createElement("span"); - indicator.className = "rg-sort-indicator"; - indicator.textContent = direction === "asc" ? "▲" : "▼"; - indicator.style.marginLeft = "4px"; - indicator.style.fontSize = "10px"; - indicator.style.color = "#666"; - indicator.style.display = "inline-block"; - indicator.style.verticalAlign = "middle"; - headerCell.appendChild(indicator); - } - }); - } catch (e) { - console.warn("updateHeaderSortIndicator error:", e); - } -} - /** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */ function createOnSortingHandler( columns: { @@ -416,6 +312,7 @@ const DataGrid = ({ getGridDataRef, cellStyleCallback, onRowClick, + statusInfo, }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); @@ -624,10 +521,10 @@ const DataGrid = ({ ) { const cell = worksheet[ - XLSX.utils.encode_cell({ - r: rowIndex, - c: col, - }) + XLSX.utils.encode_cell({ + r: rowIndex, + c: col, + }) ]; if ( cell && @@ -703,7 +600,7 @@ const DataGrid = ({ const col = range.s.c + colIndex; const cell = worksheet[ - XLSX.utils.encode_cell({ r: row, c: col }) + XLSX.utils.encode_cell({ r: row, c: col }) ]; const headerName = headers[colIndex] || `Column${colIndex + 1}`; @@ -999,7 +896,7 @@ const DataGrid = ({ ) { ( column.footer[ - index + index ] as FooterItemWithCallback ).valueCallback = footerItem.valueCallback; @@ -1050,11 +947,11 @@ const DataGrid = ({ getGridDataRef.current = () => { const gv = gridRef.current?.gridView as | { - getDataSource?: () => { - getRowCount(): number; - getValue(r: number, f: string): unknown; - }; - } + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } | undefined; if (!gv?.getDataSource) return []; const ds = gv.getDataSource(); @@ -1126,6 +1023,22 @@ const DataGrid = ({ onChange={handleFileChange} />
+ {statusInfo && +
+ 진행상황: { + statusInfo.locgStatus === null || statusInfo.locgStatus === '0' ? + '미작성' + : + statusInfo.locgStatus === '1' ? '작성중' : statusInfo.locgStatus === '2' ? '제출' + : + statusInfo.locgStatus === '3' ? '승인' : '반려' + } + + 제출일: {statusInfo.locgDt ? statusInfo.locgDt : '미제출'} + 담당자: {statusInfo.locgCharger ? statusInfo.locgCharger : '-'} + 연락처: {statusInfo.locgCttpc ? statusInfo.locgCttpc : '-'} +
+ } {isExcelImport && ( + + + + + + ); +}; + +export default SubmitDailog; \ No newline at end of file diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index eea0126c9..5635b7f8a 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -212,6 +212,10 @@ export type CellStyleCallbackParams = { export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined; +/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */ +export type CellTooltipCallbackParams = { row: Record; fieldName: string }; +export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined; + export type DataGridProps = { columnInfo: ColumnInfo; rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주) @@ -226,6 +230,8 @@ export type DataGridProps = { >; /** 셀 단위로 styleName을 동적으로 결정 (예: 검증 오류 셀) */ cellStyleCallback?: CellStyleCallback; + /** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */ + cellTooltipCallback?: CellTooltipCallback; /** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */ getGridDataRef?: { current: (() => Record[]) | null; diff --git a/apps/st/src/components/util/column/input/DoList.ts b/apps/st/src/components/util/column/input/DoList.ts new file mode 100644 index 000000000..2b58bd57b --- /dev/null +++ b/apps/st/src/components/util/column/input/DoList.ts @@ -0,0 +1,23 @@ +export const doList = [ + "4100000000", + "4200000000", + "4300000000", + "4400000000", + "4500000000", + "4600000000", + "4700000000", + "4800000000", +]; + +export const needBsnmFrgnrPoplSmSidoList = [ + "3600000000", + "4100000000", + "4200000000", + "4300000000", + "4400000000", + "4500000000", + "4600000000", + "4700000000", + "4800000000", + "5000000000", +]; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index 41a9c5700..6cd283565 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -7,13 +7,13 @@ import { ValueType } from "realgrid"; export const SeweragePenetrationRateInpColumn = { columns: [ // 행정구역 - { name: 'sido', fieldName: 'sido', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, // 수계 및 권역 { name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } }, // 수계영향권별분류 { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, styleName: 'rg-cell-readonly' }, @@ -21,7 +21,7 @@ export const SeweragePenetrationRateInpColumn = { { name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, styleName: 'rg-cell-readonly' }, // 특별대책지역 - { name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } }, + { name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } }, // 행정참고자료인구 { name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, @@ -29,113 +29,113 @@ export const SeweragePenetrationRateInpColumn = { { name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 행정참고자료면적 - { name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, // 총인구 { - name: 'popltnSm', - fieldName: 'popltnSm', + name: 'wholPpltnCnt', + fieldName: 'wholPpltnCnt', 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 }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 총면적 { - name: 'arSm', - fieldName: 'arSm', + name: 'wholArea', + fieldName: 'wholArea', header: { text: '총면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', - computed: { fields: ['swgeProcZoneInnerAr', 'swgeProcZoneOuterAr'], type: 'sum' as const }, + computed: { fields: ['staaArea', 'staaExcpArea'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 하수처리구역 내 > 합계 { - name: 'swgeProcZoneInnerSm', - fieldName: 'swgeProcZoneInnerSm', + name: 'wholSwtrmtPpltnCnt', + fieldName: 'wholSwtrmtPpltnCnt', header: { text: '합계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], type: 'sum' as const }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 하수처리구역 내 > 공공하수처리인구 { - name: 'pblswrgPopltnSm', - fieldName: 'pblswrgPopltnSm', + name: 'wholPblSwtrmtPpltnCnt', + fieldName: 'wholPblSwtrmtPpltnCnt', header: { text: '소계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], type: 'sum' as const }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'pblswrgTertTrt', fieldName: 'pblswrgTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 폐수처리인구 { - name: 'wstrPopltnSm', - fieldName: 'wstrPopltnSm', + name: 'wholWwtPrcsPpltnCnt', + fieldName: 'wholWwtPrcsPpltnCnt', header: { text: '소계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const }, + computed: { fields: ['ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'wstrTertTrt', fieldName: 'wstrTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 미접속인구 - { name: 'notConectPopltn', fieldName: 'notConectPopltn', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 면적 - { name: 'swgeProcZoneInnerAr', fieldName: 'swgeProcZoneInnerAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 하수처리구역 외 { - name: 'swgeProcZoneOuterSm', - fieldName: 'swgeProcZoneOuterSm', + name: 'wholStaaExcpPpltnCnt', + fieldName: 'wholStaaExcpPpltnCnt', header: { text: '합계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const }, + computed: { fields: ['unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'townUnprocPopltn', fieldName: 'townUnprocPopltn', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'townSwttPopltn', fieldName: 'townSwttPopltn', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'townWrrtnPopltn', fieldName: 'townWrrtnPopltn', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'swgeProcZoneOuterAr', fieldName: 'swgeProcZoneOuterAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100) { - name: 'pblswrgPopltnSuprt', - fieldName: 'pblswrgPopltnSuprt', + name: 'staaPsplyRt', + fieldName: 'staaPsplyRt', header: { text: '공공하수처리구역인구보급률(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], + numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'pblswrgPopltnSuprtRate' as const }, styleName: 'rg-cell-total', @@ -143,16 +143,16 @@ export const SeweragePenetrationRateInpColumn = { // 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100) { - name: 'adtrtPopltnSuprt', - fieldName: 'adtrtPopltnSuprt', + name: 'altdPrcsPsplyRt', + fieldName: 'altdPrcsPsplyRt', header: { text: '고도처리인구보급률(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgTertTrt', 'wstrTertTrt'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], + numeratorFields: ['ts3SwtrmtPpltnCnt', 'ts3WwtPrcsPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'adtrtPopltnSuprtRate' as const }, styleName: 'rg-cell-total', @@ -160,27 +160,27 @@ export const SeweragePenetrationRateInpColumn = { // 하수도 설치율 { - name: 'swrgInstlrt', - fieldName: 'swrgInstlrt', + name: 'sewerInstlRt', + fieldName: 'sewerInstlRt', header: { text: '하수도설치율(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'swgeProcZoneOuterSm'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], - thirdFields: ['piplSupprt'], + numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'wholStaaExcpPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], + thirdFields: ['swagePplPsplyRt'], type: 'swrgInstlrtRate' as const }, styleName: 'rg-cell-total', }, // 비고 - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - { name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로보급률(%)' }, visible: false }, - { name: 'adstCd', fieldName: 'adstCd', header: { text: '행정코드' }, visible: false }, + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false }, ], columnLayout: [ @@ -189,49 +189,49 @@ export const SeweragePenetrationRateInpColumn = { hideChildHeaders: false, header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 3 }] }, - 'gugun', - 'adstDong', + { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] }, + 'sggNm', + 'admdstNm', ], }, 'rvbsn', - 'bstrm', + 'trbtrNm', { name: 'groupB', hideChildHeaders: false, header: { text: '수계영향권별분류' }, items: ['mdbsn', 'lgbsn', 'mdmbsn'], }, - 'speclCntrplnAr', + 'spcCtpnRgnCn', { name: 'groupC', hideChildHeaders: false, header: { text: '행정참고자료인구' }, items: ['totalPopl', 'poplSm', 'frgnrPoplSm'], }, - 'area', - 'popltnSm', - 'arSm', + 'refArea', + 'wholPpltnCnt', + 'wholArea', { name: 'groupD', hideChildHeaders: false, header: { text: '하수처리구역내' }, items: [ - 'swgeProcZoneInnerSm', + 'wholSwtrmtPpltnCnt', { name: 'pblswrgGroup', hideChildHeaders: false, header: { text: '공공하수처리인구(명)' }, - items: ['pblswrgPopltnSm', 'pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], + items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], }, { name: 'wstrGroup', hideChildHeaders: false, header: { text: '폐수처리인구(명)' }, - items: ['wstrPopltnSm', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], + items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], }, - 'notConectPopltn', - 'swgeProcZoneInnerAr' + 'yetCntnPpltnCnt', + 'staaArea' ], }, { @@ -239,18 +239,18 @@ export const SeweragePenetrationRateInpColumn = { hideChildHeaders: false, header: { text: '하수처리구역외' }, items: [ - 'swgeProcZoneOuterSm', - 'townUnprocPopltn', - 'townSwttPopltn', - 'townWrrtnPopltn', - 'swgeProcZoneOuterAr' + 'wholStaaExcpPpltnCnt', + 'unprcsPpltnCnt', + 'drtwPrcsPpltnCnt', + 'spctnkPpltnCnt', + 'staaExcpArea' ], }, - 'pblswrgPopltnSuprt', - 'adtrtPopltnSuprt', - 'swrgInstlrt', - 'rmk', - 'piplSupprt', - 'adstCd', + 'staaPsplyRt', + 'altdPrcsPsplyRt', + 'sewerInstlRt', + 'rmrk', + 'swagePplPsplyRt', + 'admdstCd', ] }; \ No newline at end of file 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 f8fdc0850..a45251549 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-service.ts @@ -38,6 +38,15 @@ export class StatisticsInputAPiService implements StatisticsInputService { return response.data; } + + public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { + const response = await axios.post ( + `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`, + params + ); + + return response.data; + } } export const statisticsInputAPi = new StatisticsInputAPiService(); \ No newline at end of file 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 3fd0ba0c8..72b519acc 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-interface.ts @@ -2,120 +2,83 @@ import type { ApiResponse } from "./statistics-interface"; // 하수도 보급률 데이터 항목 export interface SewerageSupplyRateStatusInfo { - locgStatus: string; - locgDt: string; - locgCharger: string; - locgCttpc: string; - prvnStatus: string; - dcsnStatus: string; + locgStatus: string | null; + locgDt: string | null; + locgCharger: string | null; + locgCttpc: string | null; + locgJob: string | null; + prvnStatus: string | null; + dcsnStatus: string | null; wrterOpinionSm: string | null; chckerOpinionSm: string | null; returnOpinionSm: string | null; + locgCnfirmDept: string | null; + locgCnfirmJob: string | null; + locgCnfirmNm: string | null; + locgCnfirmPh: string | null; + locgDept: string | null; } export interface SewerageSupplyRateCommonInfo { - bsnmArea: number; - bsnmCd: string; - bsnmFrgnrPoplSm: number; - bsnmPoplSm: number; - bsnmTotalPopl: number; + sewerBzmnArea: number; + sewerBzmnCd: string; + sewerBzmnFrgnrPoplSm: number; + sewerBzmnPoplSm: number; + sewerBzmnTotalPopl: number; piplFcltyExtn: number; piplPlanExtn: number; - piplSupprt: number; - sido: string; - yr: string; + swagePplPsplyRt: number; + ctpvNm: string; + statsYr: string; } /** 검증 결과 한 건 (필드별 메시지) */ export interface VerificationResultItem { - type: string; - rsn: string; + vrfcCd: string; + rsnExpln: string; } /** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */ export type SewerageSupplyRateVerificationResult = Record; +/** 하수도 보급률 목록 API 응답 한 행 (실제 응답 필드만) */ export interface SewerageSupplyRateItem { - sido: string; - gugun: string; - adstDong: string; - rgnNm: string; - adstCd: string; - yr: string; - bsnmCd: string; - rvbsn: string; - lgbsn: string; + admdstCd: string; + sggNm: string; + admdstNm: string; + rvbsn: string | null; + trbtrNm: string; mdbsn: string; + lgbsn: string; mdmbsn: string; + spcCtpnRgnCn: string | null; totalPopl: number; poplSm: number; frgnrPoplSm: number; - area: number; - bsnmTotalPopl: number; - bsnmPoplSm: number; - bsnmFrgnrPoplSm: number; - bsnmArea: number; - piplPlanExtn: number; - piplFcltyExtn: number; - piplSupprt: number; - wrssm: string | null; - bstrm: string; - speclCntrplnAr: string | null; - popltnSm: number; - arSm: number; - swgeProcZoneInnerSm: number; - pblswrgPopltnSm: number; - pblswrgPrmTrt: number; - pblswrgScdTrt: number; - pblswrgTertTrt: number; - wstrPopltnSm: number; - wstrPrmTrt: number; - wstrScdTrt: number; - wstrTertTrt: number; - swgeProcZoneInnerAr: number; - swgeProcZoneOuterSm: number; - townUnprocPopltn: number; - townSwttPopltn: number; - townWrrtnPopltn: number; - uptownUnprPopltn: number; - uptownSwttPopltn: number; - uptownWrrtnPopltn: number; - swgeProcZoneOuterAr: number; - pblswrgPopltnSuprt: number; - adtrtPopltnSuprt: number; - swrgInstlrt: number; - wrterOpinion: string | null; - chckerOpinion: string | null; - notConectPopltn: number; - rmk: string; - chkWrssm: string; - chkBstrm: string; - chkSpeclCntrplnAr: string; - chkPopltnSm: string; - chkArSm: string; - chkSwgeProcZoneInnerSm: string; - chkPblswrgPopltnSm: string; - chkPblswrgPrmTrt: string; - chkPblswrgScdTrt: string; - chkPblswrgTertTrt: string; - chkWstrPopltnSm: string; - chkWstrPrmTrt: string; - chkWstrScdTrt: string; - chkWstrTertTrt: string; - chkSwgeProcZoneInnerAr: string; - chkSwgeProcZoneOuterSm: string; - chkTownUnprocPopltn: string; - chkTownSwttPopltn: string; - chkTownWrrtnPopltn: string; - chkUptownUnprPopltn: string; - chkUptownSwttPopltn: string; - chkUptownWrrtnPopltn: string; - chkSwgeProcZoneOuterAr: string; - chkPblswrgPopltnSuprt: string; - chkAdtrtPopltnSuprt: string; - chkSwrgInstlrt: string; - chkNotConectPopltn: string; - verificationResult?: SewerageSupplyRateVerificationResult; + refArea: number; + wholPpltnCnt: number; + wholArea: number; + wholSwtrmtPpltnCnt: number; + wholPblSwtrmtPpltnCnt: number; + ts1SwtrmtPpltnCnt: number; + ts2SwtrmtPpltnCnt: number; + ts3SwtrmtPpltnCnt: number; + wholWwtPrcsPpltnCnt: number; + ts1WwtPrcsPpltnCnt: number; + ts2WwtPrcsPpltnCnt: number; + ts3WwtPrcsPpltnCnt: number; + yetCntnPpltnCnt: number; + staaArea: number; + wholStaaExcpPpltnCnt: number; + unprcsPpltnCnt: number; + drtwPrcsPpltnCnt: number; + spctnkPpltnCnt: number; + staaExcpArea: number; + staaPsplyRt: number; + altdPrcsPsplyRt: number; + sewerInstlRt: number; + rmrk: string | null; + verificationResult?: SewerageSupplyRateVerificationResult | null; } export interface SewerageSupplyRateResponseData { @@ -129,55 +92,63 @@ export type SewerageSupplyRateApiResponse = ApiResponse; checkInputPeriod(params: string): Promise; saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; + submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts new file mode 100644 index 000000000..fb3380b6b --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const statisticsInputSubmitInfoSchema = z.object({ + // 작성자 명, 작성자 직제, 작성자 부서, 작성자 연락처, 승인자명, 승인자 직제, 승인자 부서, 승인자 연락처 (전부 필수) + locgCharger: z.string().min(1, "작성자 명을 입력하세요"), + locgJob: z.string().min(1, "작성자 직제를 입력하세요"), + locgDept: z.string().min(1, "작성자 부서를 입력하세요"), + locgCttpc: z.string().min(1, "작성자 연락처를 입력하세요"), + locgCnfirmNm: z.string().min(1, "승인자명을 입력하세요"), + locgCnfirmJob: z.string().min(1, "승인자 직제를 입력하세요"), + locgCnfirmDept: z.string().min(1, "승인자 부서를 입력하세요"), + locgCnfirmPh: z.string().min(1, "승인자 연락처를 입력하세요"), +}); + +export type StatisticsInputSubmitInfoFormData = z.infer< + typeof statisticsInputSubmitInfoSchema +>; \ No newline at end of file From 5600ce01bd5670374be2887aaee9ec5a5cbdb5d4 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 25 Feb 2026 16:13:48 +0900 Subject: [PATCH 018/113] =?UTF-8?q?st:=20fix:=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20mergeCell=20sort=20=EC=98=A4=EB=A5=98=20&=20ESLint?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 4 +- apps/st/src/components/module/DataGrid.tsx | 15 +- apps/st/src/components/module/TabDataGrid.tsx | 166 ++-- .../input/SeweragePenetrationRateInp.tsx | 4 +- .../statistics/review/PumpingStationRvw.tsx | 2 +- .../review/SewagePipeMaintainRvw.tsx | 32 +- .../statistics/review/SewagePipeRvw.tsx | 2 +- .../review/SewageReservoirPumpRvw.tsx | 2 +- .../review/SeweragePenetrationRateRvw.tsx | 24 +- .../statistics/review/StatsInputStatusRvw.tsx | 14 +- apps/st/src/components/type/DataGrid.type.ts | 1 + .../column/review/PumpingStationRvwColumn.ts | 2 +- .../review/SewagePipeMaintainRvwColumn.ts | 498 ++++++------ .../util/column/review/SewagePipeRvwColumn.ts | 750 +++++++++--------- .../review/SewageReservoirPumpRvwColumn.ts | 18 +- .../SeweragePenetrationRateRvwColumn.ts | 40 +- .../src/components/util/grid/footerSummary.ts | 16 +- .../api/statistics-management-service.ts | 194 ++--- .../statistics-input-review-interface.ts | 2 +- 19 files changed, 897 insertions(+), 889 deletions(-) diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 4377eb762..4e193c532 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -62,7 +62,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string }; } -const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, rowData, setRowData }: DataGridProps) => { +const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, rowData }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); @@ -255,7 +255,7 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, autoGenerateField={true} layout={columnLayout} rows={rowData} - onInitialized={(grid) => { + onInitialized={() => { const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined; if (dp?.setFields) { dp.setFields(buildFieldsFromColumns(columns)); diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index e9d4a9620..8b614f4b7 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -112,12 +112,14 @@ function createOnSortingHandler( ), ]; // mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용 - // 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용 + // 시군구(sggNm) 우선, 없으면 행정구역명(admdstNm), 없으면 첫 번째 mergeCells 컬럼 const mergeCellCols = columns.filter( (c) => c.mergeCells === true && c.fieldName, ); const mergeCellCol = - mergeCellCols.find((c) => c.fieldName === "gugun") || mergeCellCols[0]; + mergeCellCols.find((c) => c.fieldName === "sggNm") || + mergeCellCols.find((c) => c.fieldName === "admdstNm") || + mergeCellCols[0]; const groupField = mergeCellCol?.fieldName || null; const orderByField = "displayYear"; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; @@ -148,7 +150,8 @@ function createOnSortingHandler( const col = columns.find( (c) => c.fieldName === sortField || c.name === sortField, ); - if (!col || col.sortdisable) return true; + // sortdisable 컬럼은 정렬 불가(클릭 무시) + if (!col || col.sortdisable) return false; // mergeCells: true인 컬럼이 없으면 RealGrid 기본 정렬 사용 if (!hasMergeCells || !groupField) return true; @@ -173,6 +176,7 @@ function createOnSortingHandler( if (orig && setRows) { setRows(orig); setRowData(orig); + (g as { refresh?: () => void }).refresh?.(); } sortDirRef.current[sortField] = "none"; return false; @@ -191,11 +195,13 @@ function createOnSortingHandler( ); setRows?.(sorted); setRowData(sorted); + // 데이터 변경 후 merge 재적용 + (g as { refresh?: () => void }).refresh?.(); return false; }; } -/** sortPreserveGroupOrder: 그룹 간에는 sortField로 정렬, 그룹 내에서는 원본 순서 유지. */ +/** sortPreserveGroupOrder: 그룹 간에는 sortField로 정렬, 그룹 내에서는 원본 순서 유지(구분 포함). */ function sortRowsGroupPreserving( rows: Record[], sortField: string, @@ -852,6 +858,7 @@ const DataGrid = ({ ...(col.mergeCells && { mergeRule: { criteria: "value" as const }, }), + sortable: !col.sortdisable, }; if (col.footer) { diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 71d94fad3..96aec2a94 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -57,96 +57,96 @@ function toNum(v: unknown): number { } /** 헤더에 정렬 화살표 표시 (CSS 방식) */ -function updateHeaderSortIndicator( - gridView: { - columnByIndex?: (i: number) => { fieldName?: string; name?: string } | undefined; - columnByName?: (name: string) => { fieldName?: string; name?: string } | undefined; - getColumn?: (i: number) => { fieldName?: string; name?: string } | undefined; - getElement?: () => HTMLElement | null; - container?: HTMLElement; - }, - columns: { fieldName?: string; name: string }[], - sortDir: Record, - gridElement?: HTMLElement | null -) { - try { - // 그리드 DOM 요소 찾기 - let gridEl: HTMLElement | null = null; - if (gridElement) { - gridEl = gridElement; - } else if (gridView.getElement) { - gridEl = gridView.getElement(); - } else if (gridView.container) { - gridEl = gridView.container; - } else { - // 폴백: 모든 그리드 중에서 찾기 - const allGrids = document.querySelectorAll('.rg-grid, [data-grid-id]'); - if (allGrids.length > 0) { - // 활성 탭의 그리드를 찾기 위해 TabsContent 내부의 그리드 찾기 - const activeTabContent = document.querySelector('[data-state="active"]')?.closest('[role="tabpanel"]'); - if (activeTabContent) { - gridEl = activeTabContent.querySelector('.rg-grid, [data-grid-id]') as HTMLElement; - } - if (!gridEl && allGrids.length === 1) { - gridEl = allGrids[0] as HTMLElement; - } - } - } +// function updateHeaderSortIndicator( +// gridView: { +// columnByIndex?: (i: number) => { fieldName?: string; name?: string } | undefined; +// columnByName?: (name: string) => { fieldName?: string; name?: string } | undefined; +// getColumn?: (i: number) => { fieldName?: string; name?: string } | undefined; +// getElement?: () => HTMLElement | null; +// container?: HTMLElement; +// }, +// columns: { fieldName?: string; name: string }[], +// sortDir: Record, +// gridElement?: HTMLElement | null +// ) { +// try { +// // 그리드 DOM 요소 찾기 +// let gridEl: HTMLElement | null = null; +// if (gridElement) { +// gridEl = gridElement; +// } else if (gridView.getElement) { +// gridEl = gridView.getElement(); +// } else if (gridView.container) { +// gridEl = gridView.container; +// } else { +// // 폴백: 모든 그리드 중에서 찾기 +// const allGrids = document.querySelectorAll('.rg-grid, [data-grid-id]'); +// if (allGrids.length > 0) { +// // 활성 탭의 그리드를 찾기 위해 TabsContent 내부의 그리드 찾기 +// const activeTabContent = document.querySelector('[data-state="active"]')?.closest('[role="tabpanel"]'); +// if (activeTabContent) { +// gridEl = activeTabContent.querySelector('.rg-grid, [data-grid-id]') as HTMLElement; +// } +// if (!gridEl && allGrids.length === 1) { +// gridEl = allGrids[0] as HTMLElement; +// } +// } +// } - if (!gridEl) return; +// if (!gridEl) return; - columns.forEach((col) => { - if (!col.fieldName && !col.name) return; +// columns.forEach((col) => { +// if (!col.fieldName && !col.name) return; - const fieldName = col.fieldName || col.name; - const direction = sortDir[fieldName] || sortDir[col.name] || "none"; - if (direction === "none") return; +// const fieldName = col.fieldName || col.name; +// const direction = sortDir[fieldName] || sortDir[col.name] || "none"; +// if (direction === "none") return; - // 헤더 셀 찾기 (fieldName 또는 name으로) - const headerCells = gridEl.querySelectorAll('.rg-header-cell, [class*="header"], .rg-header'); - let targetCell: HTMLElement | null = null; +// // 헤더 셀 찾기 (fieldName 또는 name으로) +// const headerCells = gridEl.querySelectorAll('.rg-header-cell, [class*="header"], .rg-header'); +// let targetCell: HTMLElement | null = null; - headerCells.forEach((cell) => { - const el = cell as HTMLElement; - const text = el.textContent?.trim(); - const colName = col.name; - const colFieldName = col.fieldName; - // 헤더 텍스트와 컬럼 이름이 일치하는지 확인 - if (text && ( - text === colName || - text === colFieldName || - text.includes(colName) || - (colFieldName && text.includes(colFieldName)) - )) { - targetCell = el; - } - }); +// headerCells.forEach((cell) => { +// const el = cell as HTMLElement; +// const text = el.textContent?.trim(); +// const colName = col.name; +// const colFieldName = col.fieldName; +// // 헤더 텍스트와 컬럼 이름이 일치하는지 확인 +// if (text && ( +// text === colName || +// text === colFieldName || +// text.includes(colName) || +// (colFieldName && text.includes(colFieldName)) +// )) { +// targetCell = el; +// } +// }); - if (!targetCell) return; +// if (!targetCell) return; - const headerCell = targetCell as HTMLElement; +// const headerCell = targetCell as HTMLElement; - // 기존 화살표 제거 - const existingIndicator = headerCell.querySelector(".rg-sort-indicator") as HTMLElement | null; - if (existingIndicator) { - existingIndicator.remove(); - } +// // 기존 화살표 제거 +// const existingIndicator = headerCell.querySelector(".rg-sort-indicator") as HTMLElement | null; +// if (existingIndicator) { +// existingIndicator.remove(); +// } - // 화살표 추가 - const indicator = document.createElement("span"); - indicator.className = "rg-sort-indicator"; - indicator.textContent = direction === "asc" ? "▲" : "▼"; - indicator.style.marginLeft = "4px"; - indicator.style.fontSize = "10px"; - indicator.style.color = "#666"; - indicator.style.display = "inline-block"; - indicator.style.verticalAlign = "middle"; - headerCell.appendChild(indicator); - }); - } catch (e) { - console.warn("updateHeaderSortIndicator error:", e); - } -} +// // 화살표 추가 +// const indicator = document.createElement("span"); +// indicator.className = "rg-sort-indicator"; +// indicator.textContent = direction === "asc" ? "▲" : "▼"; +// indicator.style.marginLeft = "4px"; +// indicator.style.fontSize = "10px"; +// indicator.style.color = "#666"; +// indicator.style.display = "inline-block"; +// indicator.style.verticalAlign = "middle"; +// headerCell.appendChild(indicator); +// }); +// } catch (e) { +// console.warn("updateHeaderSortIndicator error:", e); +// } +// } /** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */ function createOnSortingHandler( @@ -154,13 +154,13 @@ function createOnSortingHandler( setRowData: React.Dispatch[]>>, sortDirRef: { current: Record }, originalRowsRef: { current: Record[]> }, - findGridElement?: () => HTMLElement | null + _findGridElement?: () => HTMLElement | null ) { const fieldNames = [...new Set(columns.map((c) => c.fieldName).filter((x): x is string => typeof x === "string"))]; // mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용 // 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용 const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName); - const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0]; + const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'sggNm') || mergeCellCols[0]; const groupField = mergeCellCol?.fieldName || null; const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 93353d927..13249fdf8 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -63,8 +63,8 @@ const SeweragePenetrationRateInp = () => { const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number; refArea?: number } | null>(null); const getGridDataRef = useRef<(() => Record[]) | null>(null); - const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm', 'refArea'] as const; - const footerWithApiDefault = (fieldName: (typeof footerFieldNames)[number], numberFormat: string) => ({ + const _footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm', 'refArea'] as const; + const footerWithApiDefault = (fieldName: (typeof _footerFieldNames)[number], numberFormat: string) => ({ expression: 'sum' as const, numberFormat, valueCallback: (grid: Parameters[0], column: Parameters[1]) => diff --git a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx index b1a1d6f9d..3fc34a034 100644 --- a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx +++ b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx @@ -16,7 +16,7 @@ import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 검토 05.중계펌프장 const PumpingStationRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - const [rowData, setRowData] = useState([]); + const [rowData, setRowData] = useState[]>([]); const [isLoading, setIsLoading] = useState(false); const columnInfo = useMemo(() => { diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index a841bc273..d8cdc7d0e 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -55,28 +55,28 @@ const SewagePipeMaintainRvw = () => { setIsLoading(true); const searchParam: SewerPipeMtcRequestParams = { - statsYr: values.year, - sewerBzmnCd: values.bsnmCd, - ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun - }; + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun + }; searchParamRef.current = searchParam; const response = await statisticsManagementApi.getSewerPipeMtcList(searchParam); if (response?.status === 'success' && response.data) { const newRowDataMap = new Map[]>(); - - const listData = Array.isArray(response.data) - ? response.data + + const listData = Array.isArray(response.data) + ? response.data : (response.data.mtcStatusList || []); - + newRowDataMap.set('ST00030001', toGridRows(listData as object[])); setRowDataMap(newRowDataMap); } else { alert(response?.message || "데이터가 없습니다."); } - } catch (error) { + } catch { alert('데이터 조회 중 오류가 발생했습니다.'); } finally { setIsLoading(false); @@ -100,18 +100,18 @@ const SewagePipeMaintainRvw = () => { if (tabKey === 'ST00030001') { response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current); - }else if (tabKey === 'ST00030002') { + } else if (tabKey === 'ST00030002') { // response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current); } if (response?.status === 'success' && response.data) { setRowDataMap(prev => { const next = new Map(prev); - - const listData = Array.isArray(response.data) - ? response.data + + const listData = Array.isArray(response.data) + ? response.data : (response.data.mtcStatusList || []); - + next.set(tabKey, toGridRows(listData as object[])); return next; }); @@ -128,7 +128,7 @@ const SewagePipeMaintainRvw = () => { return (
- +
diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 157fd87fd..fab4d2cbc 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -71,7 +71,7 @@ const SewagePipeRvw = () => { } else { alert(response?.message || "데이터가 없습니다."); } - } catch (error) { + } catch { alert('데이터 조회 중 오류가 발생했습니다.'); } finally { setIsLoading(false); diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index 346edb0cd..13303f6c1 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -75,7 +75,7 @@ const SewageReservoirPumpRvw = () => { } else { alert(response?.message || "데이터가 없습니다."); } - } catch (error) { + } catch { alert('데이터 조회 중 오류가 발생했습니다.'); } finally { setIsLoading(false); diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index 65f3cea58..18a0443cc 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -5,7 +5,7 @@ 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 { LoadingWithAnimation } from "@common/components"; import { useAlertStore } from "@/shared/lib"; import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn"; @@ -17,8 +17,8 @@ import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 검토 01.하수도 보급률 const SeweragePenetrationRateRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - const [rowData, setRowData] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); const columnInfo = useMemo(() => { const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList; @@ -49,17 +49,17 @@ const SeweragePenetrationRateRvw = () => { setIsLoading(true); const response = await statisticsManagementApi.getSewerageSupplyRateReviewList({ - ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, - sewerBzmnCd: values.bsnmCd, - statsYr: values.year, + ctpvNm: values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, }); if (response.status === 'success' && response.data) { const rawList = response.data.sewerageSupplyRateList; - const convertedData: SewerageSupplyRateReviewItem[] = rawList.map((item) => ({ + const convertedData: SewerageSupplyRateReviewItem[] = rawList.map((item) => ({ ...item, - + spcCtpnRgnCn: !item.spcCtpnRgnCn ? '해당없음' : item.spcCtpnRgnCn, })); @@ -72,7 +72,7 @@ const SeweragePenetrationRateRvw = () => { console.error("조회 중 에러 발생:", error); alert('데이터 조회 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + setIsLoading(false); } }; @@ -82,9 +82,9 @@ const SeweragePenetrationRateRvw = () => { const returnFunc = () => console.log('returnFunc'); return ( -
+
- +
diff --git a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx index 2c11f7abc..8fdd74d4f 100644 --- a/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx +++ b/apps/st/src/components/statistics/review/StatsInputStatusRvw.tsx @@ -11,12 +11,13 @@ import { useAlertStore } from "@/shared/lib"; import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import type { StatsInputStatusReviewItem } from "@/entities/statistics/model/statistics-input-review-interface"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; // 하수도 통계 검토 00. 통계 입력 현황 const StatsInputStatusRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); - const [rowData, setRowData] = useState([]); + const [rowData, setRowData] = useState[]>([]); const [isLoading, setIsLoading] = useState(false); const columnInfo = useMemo(() => { @@ -34,7 +35,7 @@ const StatsInputStatusRvw = () => { setRowData([]); }; - const handleSearch = async (values: any) => { + const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); return; @@ -46,14 +47,13 @@ const StatsInputStatusRvw = () => { const response = await statisticsManagementApi.getStatsInputStatusList({ sido: values.sido, gugun: values.gugun === 'ALL' ? '' : values.gugun, - yr: values.year || values.yr, + yr: values.year, bsnmCd: values.bsnmCd }); if (response.status === 'success' && response.data) { - const rawList = response.data.sewerageSupplyRateList || response.data; - - const convertedData: StatsInputStatusReviewItem[] = rawList.map((item: any) => ({ + const rawList = response.data.sewerageSupplyRateList ?? []; + const convertedData: StatsInputStatusReviewItem[] = rawList.map((item: StatsInputStatusReviewItem) => ({ ...item, status: item.status || '미확인', })); diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 5635b7f8a..69d82b6d2 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -13,6 +13,7 @@ export type RealGridColumnInfo = { dataFormat?: string; styleName?: string; mergeRule?: { criteria: "value" }; + sortable?: boolean; footer?: | { text?: string; diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index f031fed5e..b42f1637a 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -1,6 +1,6 @@ import { ValueType } from "realgrid"; import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; -import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; +import type { GridView } from "@/components/type/DataGrid.type"; export const rawPumpingStationInfoList = { columns: [ diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index a9433e643..cbc536253 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -1,277 +1,277 @@ import { ValueType } from "realgrid"; -import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; -import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; +import { footerIntArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; +import type { GridView } from "@/components/type/DataGrid.type"; // 하수도 통계 관리 03. 하수관로 유지 관리 const rawMaintainList = [ - { - tabKey: 'ST00030001', - tabLabel: '개보수', - rowStartNum: 4, - fixedColumns: 2, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - 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)); + { + tabKey: 'ST00030001', + tabLabel: '개보수', + rowStartNum: 4, + fixedColumns: 2, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - 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: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식\n맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }, { colspan: 2 }] }, - 'sggNm', - 'admdstNm', - ] - }, - 'displayYear', - { - name: 'mtcPipeGroup', header: { text: '개·보수관로' }, - items: [ - { - name: 'totalRepairGroup', header: { text: '전체보수(m)' }, - items: ['mendngWhol', 'cbsysMendngPrlg', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }] - }, - { - name: 'partRepairGroup', header: { text: '부분보수(개소)' }, - items: ['partMendngWhol', 'cbsysPartMendngPrlg', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg'] }] - } - ] - }, - 'cbsysMnhlCnt', - { name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] }, - 'drtwRcvprpCnt', 'exlnRcvprpCnt', - { name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['stofcbrCnt', 'otflCnt'] }, - 'cctvExmnPrlg', 'rmrk' - ] - }, - { - tabKey: 'ST00030002', - tabLabel: '관로준설', - rowStartNum: 4, - fixedColumns: 2, - columns: [ - /* 1. 행정 및 기본 정보 */ - { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - 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) uniqueValues.add(String(divisionValue)); + ] + }, + { name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식\n맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }, { colspan: 2 }] }, + 'sggNm', + 'admdstNm', + ] + }, + 'displayYear', + { + name: 'mtcPipeGroup', header: { text: '개·보수관로' }, + items: [ + { + name: 'totalRepairGroup', header: { text: '전체보수(m)' }, + items: ['mendngWhol', 'cbsysMendngPrlg', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }] + }, + { + name: 'partRepairGroup', header: { text: '부분보수(개소)' }, + items: ['partMendngWhol', 'cbsysPartMendngPrlg', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg'] }] + } + ] + }, + 'cbsysMnhlCnt', + { name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] }, + 'drtwRcvprpCnt', 'exlnRcvprpCnt', + { name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['stofcbrCnt', 'otflCnt'] }, + 'cctvExmnPrlg', 'rmrk' + ] + }, + { + tabKey: 'ST00030002', + tabLabel: '관로준설', + rowStartNum: 4, + fixedColumns: 2, + columns: [ + /* 1. 행정 및 기본 정보 */ + { + name: 'sido', + fieldName: 'sido', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) 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) uniqueValues.add(String(divisionValue)); + }, + { + valueCallback: (grid: GridView) => { + 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) 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: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const uniqueValues = new Set(); - for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { - const val = grid.getDataSource().getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + ] + }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const uniqueValues = new Set(); + for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { + const val = grid.getDataSource().getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > 0 ? divisionValues[0] + ' 합계' : ''; } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > 0 ? divisionValues[0] + ' 합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const uniqueValues = new Set(); - for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { - const val = grid.getDataSource().getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + }, + { + valueCallback: (grid: GridView) => { + const uniqueValues = new Set(); + for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { + const val = grid.getDataSource().getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > 1 ? divisionValues[1] + ' 합계' : ''; } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > 1 ? divisionValues[1] + ' 합계' : ''; } - } - ] - }, - - /* 2. 시설 기본 정보 */ - { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + ] + }, - /* 3. 준설연장 및 개소수 */ - { name: 'drdgPipeSm', fieldName: 'drdgPipeSm', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgCmbnPipe', fieldName: 'drdgCmbnPipe', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgRainwtrPipe', fieldName: 'drdgRainwtrPipe', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgPltwtrPipe', fieldName: 'drdgPltwtrPipe', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'soilspllSoilholeSite', fieldName: 'soilspllSoilholeSite', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 2. 시설 기본 정보 */ + { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 4. 준설 계획 및 율 */ - { name: 'drdgTrgetExtn', fieldName: 'drdgTrgetExtn', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgTrgetRt', fieldName: 'drdgTrgetRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'piplFcltyRt', fieldName: 'piplFcltyRt', header: { text: '준설계획\n연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 3. 준설연장 및 개소수 */ + { name: 'drdgPipeSm', fieldName: 'drdgPipeSm', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgCmbnPipe', fieldName: 'drdgCmbnPipe', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgRainwtrPipe', fieldName: 'drdgRainwtrPipe', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgPltwtrPipe', fieldName: 'drdgPltwtrPipe', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'soilspllSoilholeSite', fieldName: 'soilspllSoilholeSite', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 5. 준설실적 */ - { name: 'drdgAcmsltAmount', fieldName: 'drdgAcmsltAmount', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drdgAcmsltQy', fieldName: 'drdgAcmsltQy', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 4. 준설 계획 및 율 */ + { name: 'drdgTrgetExtn', fieldName: 'drdgTrgetExtn', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgTrgetRt', fieldName: 'drdgTrgetRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'piplFcltyRt', fieldName: 'piplFcltyRt', header: { text: '준설계획\n연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 6. 준설토사처리 */ - { name: 'drdgAcmsltUntpc', fieldName: 'drdgAcmsltUntpc', header: { text: '준설토\n처리비\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drdgEtndSm', fieldName: 'drdgEtndSm', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rclm', fieldName: 'rclm', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'oceanDpsl', fieldName: 'oceanDpsl', header: { text: '해양투기' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ruse', fieldName: 'ruse', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'etc', fieldName: 'etc', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 5. 준설실적 */ + { name: 'drdgAcmsltAmount', fieldName: 'drdgAcmsltAmount', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drdgAcmsltQy', fieldName: 'drdgAcmsltQy', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 7. 기타 및 의견 */ - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: [{ column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'gugun'] }, - 'displayYear', 'piplFcltyExtn', - { - name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, - items: [ - { - name: 'pipeLenGroup', header: { text: '관로(m)' }, - items: ['drdgPipeSm', 'drdgCmbnPipe', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['drdgRainwtrPipe', 'drdgPltwtrPipe'] }] - }, - 'mnhleSite', 'pltwtrRcvrSite', 'soilspllSoilholeSite' - ] - }, - 'drdgTrgetExtn', 'drdgTrgetRt', 'piplFcltyRt', - { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgAcmsltAmount', 'drdgAcmsltQy'] }, - { - name: 'processGroup', header: { text: '준설토사처리' }, - items: ['drdgAcmsltUntpc', 'drdgEtndSm', 'rclm', 'oceanDpsl', 'ruse', 'etc'] - }, - 'rmk' - ] -} + /* 6. 준설토사처리 */ + { name: 'drdgAcmsltUntpc', fieldName: 'drdgAcmsltUntpc', header: { text: '준설토\n처리비\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drdgEtndSm', fieldName: 'drdgEtndSm', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rclm', fieldName: 'rclm', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'oceanDpsl', fieldName: 'oceanDpsl', header: { text: '해양투기' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'ruse', fieldName: 'ruse', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etc', fieldName: 'etc', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + /* 7. 기타 및 의견 */ + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { name: 'addrGroup', header: { text: '행정구역' }, items: [{ column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'gugun'] }, + 'displayYear', 'piplFcltyExtn', + { + name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, + items: [ + { + name: 'pipeLenGroup', header: { text: '관로(m)' }, + items: ['drdgPipeSm', 'drdgCmbnPipe', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['drdgRainwtrPipe', 'drdgPltwtrPipe'] }] + }, + 'mnhleSite', 'pltwtrRcvrSite', 'soilspllSoilholeSite' + ] + }, + 'drdgTrgetExtn', 'drdgTrgetRt', 'piplFcltyRt', + { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgAcmsltAmount', 'drdgAcmsltQy'] }, + { + name: 'processGroup', header: { text: '준설토사처리' }, + items: ['drdgAcmsltUntpc', 'drdgEtndSm', 'rclm', 'oceanDpsl', 'ruse', 'etc'] + }, + 'rmk' + ] + } ]; export const SewagePipeMaintainColumnInfoList = rawMaintainList.map(tab => ({ diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index db5034cb3..cc86bd201 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -1,22 +1,22 @@ import { ValueType } from "realgrid"; const baseYear = new Date().getFullYear(); import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; -import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; +import type { GridView } from "@/components/type/DataGrid.type"; // 하수도 통계 관리 02. 하수관로 const rawList = [ - { + { tabKey: 'ST00020001', tabLabel: '지역별 시설현황', rowStartNum: 4, fixedColumns: 2, columns: [ { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, footer: [ { valueCallback: (grid: GridView) => { @@ -170,392 +170,392 @@ const rawList = [ 'pltwtrRcvrCnt', 'rainwtrRcvrCnt', 'rainwtrChmbrCnt', 'rainwtrOutltCnt', 'swrgRegstrCmptExtn', 'swrgRegstrCmptRt', 'rmrk' ] }, - { - tabKey: 'ST00020002', - tabLabel: '시설별 설치현황', - rowStartNum: 4, - fixedColumns: 3, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - 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)); + { + tabKey: 'ST00020002', + tabLabel: '시설별 설치현황', + rowStartNum: 4, + fixedColumns: 3, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명칭' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + ] + }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명칭' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; - } - }, - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; } - } - ] - }, - { name: 'swrmtFcltyCpc', fieldName: 'swrmtFcltyCpc', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'procZoneArSm', fieldName: 'procZoneArSm', 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: 'sprsysProcZoneAr', fieldName: 'sprsysProcZoneAr', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { 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: 'sprsysDrngEqpSm', fieldName: 'sprsysDrngEqpSm', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'swrgPipeFcltyExtn', fieldName: 'swrgPipeFcltyExtn', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'icgPltwtrMnnSm', fieldName: 'icgPltwtrMnnSm', 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: 'pltwtrMnnExtn', fieldName: 'pltwtrMnnExtn', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnRainwtrChmbrCnt', fieldName: 'cmbnRainwtrChmbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysRainwtrChmbrCnt', fieldName: 'sprsysRainwtrChmbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysRainwtrOutltCnt', fieldName: 'sprsysRainwtrOutltCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { 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: 'sprsysExlnBrnchlnExtn', fieldName: 'sprsysExlnBrnchlnExtn', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'sprsysDrtwBrnchlnExtn', fieldName: 'sprsysDrtwBrnchlnExtn', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, - 'sggNm', - 'swrmtFcltyNm', - ] - }, - 'swrmtFcltyCpc', - 'displayYear', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['procZoneArSm', 'cmbnProcZoneAr', 'sprsysProcZoneAr'] }, - { - name: 'drainGroup', header: { text: '배수설비(개소)' }, - items: ['drngEqpSm', 'cmbnDrngEqp', 'sprsysDrngEqpSm'], - }, - { - name: 'pipeGroup', header: { text: '하수관로' }, - items: [ - 'swrgPipeFcltyExtn', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['icgPltwtrMnnSm', 'icgExtn', 'pltwtrMnnExtn'] }, - { - name: 'siteGroup', header: { text: '토실/토구(개소)' }, - items: [ - { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cmbnRainwtrChmbrCnt'] }, - { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysRainwtrChmbrCnt', 'sprsysRainwtrOutltCnt'] } - ] - }, - { - name: 'branchGroup', header: { text: '지선(m)' }, - items: ['brnchlnSm', 'cmbnBrnchlnExtn', { name: 'sepBranchGroup', header: { text: '우수/오수' }, items: ['sprsysExlnBrnchlnExtn', 'sprsysDrtwBrnchlnExtn'] }] - } - ], - }, - 'rmrk' - ] -}, - { - tabKey: 'ST00020003', - tabLabel: '관종별 현황', - rowStartNum: 2, - fixedColumns: 3, - columns: [ - { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + ] + }, + { name: 'swrmtFcltyCpc', fieldName: 'swrmtFcltyCpc', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'procZoneArSm', fieldName: 'procZoneArSm', 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: 'sprsysProcZoneAr', fieldName: 'sprsysProcZoneAr', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { 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: 'sprsysDrngEqpSm', fieldName: 'sprsysDrngEqpSm', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'swrgPipeFcltyExtn', fieldName: 'swrgPipeFcltyExtn', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'icgPltwtrMnnSm', fieldName: 'icgPltwtrMnnSm', 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: 'pltwtrMnnExtn', fieldName: 'pltwtrMnnExtn', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cmbnRainwtrChmbrCnt', fieldName: 'cmbnRainwtrChmbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysRainwtrChmbrCnt', fieldName: 'sprsysRainwtrChmbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysRainwtrOutltCnt', fieldName: 'sprsysRainwtrOutltCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { 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: 'sprsysExlnBrnchlnExtn', fieldName: 'sprsysExlnBrnchlnExtn', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'sprsysDrtwBrnchlnExtn', fieldName: 'sprsysDrtwBrnchlnExtn', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + 'sggNm', + 'swrmtFcltyNm', + ] + }, + 'swrmtFcltyCpc', + 'displayYear', + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['procZoneArSm', 'cmbnProcZoneAr', 'sprsysProcZoneAr'] }, + { + name: 'drainGroup', header: { text: '배수설비(개소)' }, + items: ['drngEqpSm', 'cmbnDrngEqp', 'sprsysDrngEqpSm'], + }, + { + name: 'pipeGroup', header: { text: '하수관로' }, + items: [ + 'swrgPipeFcltyExtn', + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['icgPltwtrMnnSm', 'icgExtn', 'pltwtrMnnExtn'] }, + { + name: 'siteGroup', header: { text: '토실/토구(개소)' }, + items: [ + { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cmbnRainwtrChmbrCnt'] }, + { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysRainwtrChmbrCnt', 'sprsysRainwtrOutltCnt'] } + ] + }, + { + name: 'branchGroup', header: { text: '지선(m)' }, + items: ['brnchlnSm', 'cmbnBrnchlnExtn', { name: 'sepBranchGroup', header: { text: '우수/오수' }, items: ['sprsysExlnBrnchlnExtn', 'sprsysDrtwBrnchlnExtn'] }] + } + ], + }, + 'rmrk' + ] + }, + { + tabKey: 'ST00020003', + tabLabel: '관종별 현황', + rowStartNum: 2, + fixedColumns: 3, + columns: [ + { + name: 'sido', + fieldName: 'sido', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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 uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true,sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + ] + }, + { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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 uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (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: 'totalExtnSum', fieldName: 'totalExtnSum', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'layingExtnSum', fieldName: 'layingExtnSum', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'remvlExtnSum', fieldName: 'remvlExtnSum', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 총계 필드 */ + { name: 'totalExtnSum', fieldName: 'totalExtnSum', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'layingExtnSum', fieldName: 'layingExtnSum', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'remvlExtnSum', fieldName: 'remvlExtnSum', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - const yrOffset = 29 - i; - return [ - { name: `totalExtn${yNum}y`, fieldName: `totalExtn${yNum}y`, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `layingExtn${yNum}y`, fieldName: `layingExtn${yNum}y`, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `remvlExtn${yNum}y`, fieldName: `remvlExtn${yNum}y`, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } - ]; - }).flat(), + ...Array.from({ length: 30 }, (_, i) => { + const yNum = (30 - i).toString().padStart(2, '0'); + // const yrOffset = 29 - i; + return [ + { name: `totalExtn${yNum}y`, fieldName: `totalExtn${yNum}y`, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `layingExtn${yNum}y`, fieldName: `layingExtn${yNum}y`, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `remvlExtn${yNum}y`, fieldName: `remvlExtn${yNum}y`, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + ]; + }).flat(), - { name: 'layingExtn', fieldName: 'layingExtn', header: { text: '매설연장(m)\n(당해년도)' }, 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', - 'swrgPipeNm', - ] - }, - 'displayYear', - { name: 'summaryGroup', header: { text: '총계' }, items: ['totalExtnSum', 'layingExtnSum', 'remvlExtnSum'] }, - - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - const yrOffset = 29 - i; - return { - name: `yr${yNum}Group`, - header: { text: `-${yrOffset}년` }, - items: [`totalExtn${yNum}y`, `layingExtn${yNum}y`, `remvlExtn${yNum}y`] - }; - }), - - { name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['layingExtn'] }, - 'rmk' - ] -}, - { - tabKey: 'ST00020004', - tabLabel: '관경별 현황', - rowStartNum: 2, - fixedColumns: 3, - columns: [ - { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - 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, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - 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: 'layingExtn', fieldName: 'layingExtn', header: { text: '매설연장(m)\n(당해년도)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'areaGroup', + header: { text: '행정구역' }, + items: [ - { name: 'totalExtnM', fieldName: 'totalExtnM', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'under200', fieldName: 'under200', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over200Undev300', fieldName: 'over200Undev300', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over300Undev500', fieldName: 'over300Undev500', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over500Undev500', fieldName: 'over500Undev500', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over800', fieldName: 'over800', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'box', fieldName: 'box', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'etc', fieldName: 'etc', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'gugun', - 'swrgPipeNm', - ] - }, - 'displayYear', - 'totalExtnM', - { - name: 'circleGroup', - header: { text: '원형(m)' }, - items: ['under200', 'over200Undev300', 'over300Undev500', 'over500Undev500', 'over800'] - }, - 'box', - 'etc', - 'rmk' - ] -} + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'swrgPipeNm', + ] + }, + 'displayYear', + { name: 'summaryGroup', header: { text: '총계' }, items: ['totalExtnSum', 'layingExtnSum', 'remvlExtnSum'] }, + + ...Array.from({ length: 30 }, (_, i) => { + const yNum = (30 - i).toString().padStart(2, '0'); + const yrOffset = 29 - i; + return { + name: `yr${yNum}Group`, + header: { text: `-${yrOffset}년` }, + items: [`totalExtn${yNum}y`, `layingExtn${yNum}y`, `remvlExtn${yNum}y`] + }; + }), + + { name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['layingExtn'] }, + 'rmk' + ] + }, + { + tabKey: 'ST00020004', + tabLabel: '관경별 현황', + rowStartNum: 2, + fixedColumns: 3, + columns: [ + { + name: 'sido', + fieldName: 'sido', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'totalExtnM', fieldName: 'totalExtnM', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'under200', fieldName: 'under200', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over200Undev300', fieldName: 'over200Undev300', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over300Undev500', fieldName: 'over300Undev500', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over500Undev500', fieldName: 'over500Undev500', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'over800', fieldName: 'over800', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'box', fieldName: 'box', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etc', fieldName: 'etc', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'gugun', + 'swrgPipeNm', + ] + }, + 'displayYear', + 'totalExtnM', + { + name: 'circleGroup', + header: { text: '원형(m)' }, + items: ['under200', 'over200Undev300', 'over300Undev500', 'over500Undev500', 'over800'] + }, + 'box', + 'etc', + 'rmk' + ] + } ]; export const SewagePipeColumnInfoListRvw = rawList.map(tab => ({ diff --git a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts index 415d2896f..83d0ab65a 100644 --- a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts @@ -5,7 +5,7 @@ import { footerDecimal1ArrayByDivision, footerDecimal3ArrayByDivision } from "@/components/util/grid/footerSummary"; -import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; +import type { GridView } from "@/components/type/DataGrid.type"; // 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장 const rawReservoirList = [ @@ -24,7 +24,7 @@ const rawReservoirList = [ sortdisable: true, footer: [ { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 0; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -41,7 +41,7 @@ const rawReservoirList = [ } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 1; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -69,7 +69,7 @@ const rawReservoirList = [ sortdisable: true, footer: [ { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 0; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -85,7 +85,7 @@ const rawReservoirList = [ } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 1; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -190,7 +190,7 @@ const rawReservoirList = [ sortdisable: true, footer: [ { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 0; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -207,7 +207,7 @@ const rawReservoirList = [ } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 1; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -235,7 +235,7 @@ const rawReservoirList = [ sortdisable: true, footer: [ { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 0; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); @@ -251,7 +251,7 @@ const rawReservoirList = [ } }, { - valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => { + valueCallback: (grid: GridView) => { const divisionIndex = 1; const dataSource = grid.getDataSource(); const rowCount = dataSource.getRowCount(); diff --git a/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts b/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts index 490e79099..25449c6cc 100644 --- a/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SeweragePenetrationRateRvwColumn.ts @@ -1,16 +1,16 @@ import { ValueType } from "realgrid"; import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; -import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type"; +import type { GridView } from "@/components/type/DataGrid.type"; const rawSeweragePenetrationRateInfoList = { - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, footer: [ { valueCallback: (grid: GridView) => { @@ -18,7 +18,7 @@ const rawSeweragePenetrationRateInfoList = { const dataSource = grid.getDataSource(); const uniqueValues = new Set(); for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); + const val = dataSource.getValue(i, 'displayYear'); if (val) uniqueValues.add(String(val)); } const divisionValues = Array.from(uniqueValues); @@ -40,12 +40,12 @@ const rawSeweragePenetrationRateInfoList = { } ] }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, sortdisable: true, footer: [ { @@ -76,9 +76,9 @@ const rawSeweragePenetrationRateInfoList = { } ] }, - { name: 'wtsysNm', fieldName: 'rvbsn', header: { text: '수계' } }, + { name: 'wtsysNm', fieldName: 'rvbsn', header: { text: '수계' } }, { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } }, - { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' } }, + { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' } }, { name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' } }, { name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' } }, { name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } }, @@ -111,9 +111,9 @@ const rawSeweragePenetrationRateInfoList = { hideChildHeaders: false, header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', - 'admdstNm', + 'admdstNm', ] }, 'displayYear', @@ -161,7 +161,7 @@ const rawSeweragePenetrationRateInfoList = { 'sewerInstlRt', 'rmrk', ], - fixedColumns: 2, + fixedColumns: 2, rowStartNum: 3 }; diff --git a/apps/st/src/components/util/grid/footerSummary.ts b/apps/st/src/components/util/grid/footerSummary.ts index 0b9bd148b..530d65f8c 100644 --- a/apps/st/src/components/util/grid/footerSummary.ts +++ b/apps/st/src/components/util/grid/footerSummary.ts @@ -181,8 +181,8 @@ const createSumFooterArrayByDivision = (format: string, divisionFieldName: strin grid: GridView, column: DataColumn, footerIndex: number, - columnFooter: unknown, - value: number | string | undefined + // columnFooter: unknown, + // value: number | string | undefined ) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, { @@ -191,8 +191,8 @@ const createSumFooterArrayByDivision = (format: string, divisionFieldName: strin grid: GridView, column: DataColumn, footerIndex: number, - columnFooter: unknown, - value: number | string | undefined + // columnFooter: unknown, + // value: number | string | undefined ) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, ]; @@ -205,8 +205,8 @@ const createAvgFooterArrayByDivision = (format: string, divisionFieldName: strin grid: GridView, column: DataColumn, footerIndex: number, - columnFooter: unknown, - value: number | string | undefined + // columnFooter: unknown, + // value: number | string | undefined ) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, { @@ -215,8 +215,8 @@ const createAvgFooterArrayByDivision = (format: string, divisionFieldName: strin grid: GridView, column: DataColumn, footerIndex: number, - columnFooter: unknown, - value: number | string | undefined + // columnFooter: unknown, + // value: number | string | undefined ) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName), }, ]; 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 0e67b89fe..f7a75da29 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -2,31 +2,31 @@ import axios from "axios"; import { ENDPOINT } from "@/shared/config"; -import { - type StatsInputStatusReviewApiResponse -} from "../model/statistics-input-review-interface"; -import { +import { type CheckInputPeriodResponse } from "../model/statistics-input-interface"; +import { + type StatsInputStatusReviewApiResponse +} from "../model/statistics-input-review-interface"; +import { type SewerageSupplyRateReviewRequestParams, type SewerageSupplyRateReviewApiResponse, -} from "../model/statistics-sewerage-penetration-review-interface"; -import { +} from "../model/statistics-sewerage-penetration-review-interface"; +import { type SewagePipeReviewRequestParams, - type SewagePipeReviewRegionalApiResponse , + type SewagePipeReviewRegionalApiResponse, type SewagePipeFacilityApiResponse, type SewerPipeTypeApiResponse, type SewerPipeDiamApiResponse, - type SewagePipeReviewService } from "../model/statistics-sewage-pipe-review-interface"; -import { +import { type SewerPipeMtcApiResponse, type SewerPipeDrdgApiResponse, type SewerPipeMtcRequestParams } from "../model/statistics-sewage-pipe-mtc-review-interface"; -import { +import { type SewerReservoirRegionalApiResponse, type PumpingStationRegionalApiResponse } from "../model/sewage-reservoir-pump-review-interface"; -import { +import { type MediationPumpStationApiResponse, } from "../model/statistics-pumping-station-review-interface"; @@ -50,8 +50,8 @@ export class StatisticsManagementApiService { * 입력 기간 확인 (공통) * @param year 연도 */ - public async checkInputPeriod(year: string | number): Promise { - const response = await axios.get( + public async checkInputPeriod(year: string | number): Promise { + const response = await axios.get( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period`, { params: { year } } ); @@ -80,7 +80,7 @@ export class StatisticsManagementApiService { params: SewagePipeReviewRequestParams ): Promise { const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/regional-status`, + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/regional-status`, { params } ); return response.data; @@ -94,16 +94,16 @@ export class StatisticsManagementApiService { params: SewagePipeReviewRequestParams ): Promise { const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/facility-status`, + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/facility-status`, { params } ); return response.data; } - /** - * 02. 하수관로 관종별 시설현황 조회 (Tab3) - * @param params (sido, gugun, yr, bsnmCd) - */ + /** + * 02. 하수관로 관종별 시설현황 조회 (Tab3) + * @param params (sido, gugun, yr, bsnmCd) + */ public async getSewagePipeTypeList( params: { yr?: string; @@ -113,7 +113,7 @@ export class StatisticsManagementApiService { } ): Promise { const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/type-status`, + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/type-status`, { params } ); return response.data; @@ -132,7 +132,7 @@ export class StatisticsManagementApiService { } ): Promise { const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/diam-status`, + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/diam-status`, { params } ); return response.data; @@ -145,89 +145,89 @@ export class StatisticsManagementApiService { public async getSewerPipeMtcList( params: SewerPipeMtcRequestParams ): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/repair`, - { params } - ); - return response.data; -} - -/** - * 03. 하수관로 유지관리 관로준설 현황 조회 (Tab B) - * @param params (yr, bsnmCd, sido, gugun) - */ -public async getSewerPipeDrdgList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/repair`, + { params } + ); + return response.data; } -): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/dredging`, - { params } - ); - return response.data; -} -/** - * 04. 하수저류시설 tab1. 시설현황 조회 - * @param params (yr, bsnmCd, sido, gugun) - */ -public async getSewerReservoirList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; + /** + * 03. 하수관로 유지관리 관로준설 현황 조회 (Tab B) + * @param params (yr, bsnmCd, sido, gugun) + */ + public async getSewerPipeDrdgList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/dredging`, + { params } + ); + return response.data; } -): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/facilities`, - { params } - ); - return response.data; -} -/** - * 04. 빗물펌프장 tab2. 조회 - * @param params (yr, bsnmCd, sido, gugun) - */ -public async getPumpStationList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; + /** + * 04. 하수저류시설 tab1. 시설현황 조회 + * @param params (yr, bsnmCd, sido, gugun) + */ + public async getSewerReservoirList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/facilities`, + { params } + ); + return response.data; } -): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/pumping-stations`, - { params } - ); - return response.data; -} -/** - * 05. 중계펌프장 통계관리 조회 - * @param params (yr, bsnmCd, sido, gugun, bscFctNm) - */ -public async getMediationPumpStationList( - params: { - yr?: string; // 조회 연도 - bsnmCd?: string; // 사업소 코드 - sido?: string; // 시도 - gugun?: string; // 구군 - bscFctNm?: string; // 펌프장명 (시설명) + /** + * 04. 빗물펌프장 tab2. 조회 + * @param params (yr, bsnmCd, sido, gugun) + */ + public async getPumpStationList( + params: { + yr?: string; + bsnmCd?: string; + sido?: string; + gugun?: string; + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/pumping-stations`, + { params } + ); + return response.data; + } + + /** + * 05. 중계펌프장 통계관리 조회 + * @param params (yr, bsnmCd, sido, gugun, bscFctNm) + */ + public async getMediationPumpStationList( + params: { + yr?: string; // 조회 연도 + bsnmCd?: string; // 사업소 코드 + sido?: string; // 시도 + gugun?: string; // 구군 + bscFctNm?: string; // 펌프장명 (시설명) + } + ): Promise { + const response = await axios.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/mediation-pump-stations`, + { params } + ); + return response.data; } -): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.REVIEW}/mediation-pump-stations`, - { params } - ); - return response.data; -} } // 통합 인스턴스 수출 diff --git a/apps/st/src/entities/statistics/model/statistics-input-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-review-interface.ts index 93f1a4651..ff86a8799 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-review-interface.ts @@ -104,7 +104,7 @@ export type StatsInputStatusReviewApiResponse = ApiResponse Date: Wed, 25 Feb 2026 17:04:27 +0900 Subject: [PATCH 019/113] =?UTF-8?q?st:=20fix:=20=ED=95=98=EC=88=98?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=20=EA=B2=80=ED=86=A0=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20&=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=ED=8C=90=EB=B3=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=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 | 6 ++-- .../input/SeweragePenetrationRateInp.tsx | 28 ++++++++++-------- apps/st/src/components/type/DataGrid.type.ts | 4 ++- .../util/column/review/SewagePipeRvwColumn.ts | 6 ++-- .../pages/statistics/ui/statistics-page.tsx | 6 ++++ apps/st/src/store/admin-store.ts | 29 +++++++++++++++++++ 6 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 apps/st/src/store/admin-store.ts diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 96aec2a94..b188ce3fe 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -708,7 +708,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r valueType: col.valueType, dataFormat: col.dataFormat, ...(col.styleName != null && { styleName: col.styleName }), - ...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }), + ...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), }; if (col.footer) { if (Array.isArray(col.footer)) c.footer = col.footer; @@ -993,7 +993,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r valueType: col.valueType, dataFormat: col.dataFormat, ...(col.styleName != null && { styleName: col.styleName }), - ...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }), + ...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), }; if (col.footer) { @@ -1151,7 +1151,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r expression: (column.footer as { expression?: 'sum' | 'avg' | 'count' | 'max' | 'min' }).expression, numberFormat: (column.footer as { numberFormat?: string }).numberFormat, } : undefined} - mergeRule={column.mergeCells ? { criteria: 'value' } : undefined} + mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined} sortable={!column.sortdisable} /> ))} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 13249fdf8..cf7e52e27 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -12,6 +12,7 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import SubmitDailog from "./SubmitDailog"; import { useAlertStore } from "@/shared/lib"; +import { useAdminStore } from "@/store/admin-store"; import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { @@ -60,6 +61,8 @@ const SeweragePenetrationRateInp = () => { const [showDailog, setShowDailog] = useState(false); const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number; refArea?: number } | null>(null); const getGridDataRef = useRef<(() => Record[]) | null>(null); @@ -74,7 +77,7 @@ const SeweragePenetrationRateInp = () => { // 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요. const columnInfo = useMemo(() => { const { columns, columnLayout } = SeweragePenetrationRateInpColumn; - const base = isEnterable ? columns : columns.map((col) => ({ ...col, editable: false as const })); + const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const })); return { columnLayout, columns: base.map((col) => { @@ -85,7 +88,7 @@ const SeweragePenetrationRateInp = () => { return col; }), }; - }, [isEnterable, footerFromApi]); + }, [isEnterable, isAdminInfo, footerFromApi]); const handleReset = () => { setRowData([]); @@ -389,26 +392,27 @@ const SeweragePenetrationRateInp = () => { <>
- {rowData.length > 0 && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && + {rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && (
- - getLastYearDataFunc(searchValues ?? {})} /> - - { - // 최고 관리자일땐 저장 가능하게 - - } + {isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && ( + <> + + getLastYearDataFunc(searchValues ?? {})} /> + + + )} + {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && }
- } + )} {isLoading &&
} 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} rowStartNum={3} diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 69d82b6d2..0106251d5 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -12,7 +12,7 @@ export type RealGridColumnInfo = { valueType?: import("realgrid").ValueType; dataFormat?: string; styleName?: string; - mergeRule?: { criteria: "value" }; + mergeRule?: { criteria: "value" | string }; sortable?: boolean; footer?: | { @@ -135,6 +135,8 @@ export type ColumnConfig = { /** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */ styleName?: string; mergeCells?: boolean; + /** 병합 시 그룹 기준 필드(예: 시군구). 지정 시 같은 그룹 내에서만 value 기준 병합 */ + mergeGroupField?: string; sortdisable?: boolean; sortPreserveGroupOrder?: SortPreserveGroupOrder; valueType?: ValueType; diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index cc86bd201..1020ce3bc 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -218,7 +218,8 @@ const rawList = [ ] }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명칭' }, mergeCells: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, { name: 'displayYear', fieldName: 'displayYear', @@ -276,8 +277,9 @@ const rawList = [ name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', + 'admdstNm', 'swrmtFcltyNm', ] }, diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 7513b0ffe..a228a4d46 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -3,6 +3,7 @@ import { Suspense, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; import { useTabStore } from "@/store/tab-store"; +import { useAdminStore } from "@/store/admin-store"; import Dashboard from "@/components/Dashboard"; import { SkeletonPage } from "@/shared/ui/skeleton-page"; @@ -10,6 +11,7 @@ import "@rsone/components/index.css"; const StatisticsPage = () => { const { tabs, activeTabId } = useTabStore(); + const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo); // 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨) const componentCache = useRef(new Map()); @@ -26,6 +28,10 @@ const StatisticsPage = () => { }); }, [tabs]); + useEffect(() => { + fetchAdminInfo(); + }, [fetchAdminInfo]); + // 탭이 없으면 Dashboard 표시 if (tabs.length === 0) { return ( diff --git a/apps/st/src/store/admin-store.ts b/apps/st/src/store/admin-store.ts new file mode 100644 index 000000000..1b9937d4f --- /dev/null +++ b/apps/st/src/store/admin-store.ts @@ -0,0 +1,29 @@ +import { create } from "zustand"; +import { UserApi } from "@/entities/user"; +import { getUserInfo } from "../../../../packages/common/components/src/api"; + +interface AdminStore { + /** 현재 사용자가 관리자 목록에 있는지 */ + isAdminInfo: boolean; + setAdminInfo: (value: boolean) => void; + /** API로 관리자 목록 조회 후 현재 사용자 일치 여부 설정 */ + fetchAdminInfo: () => Promise; +} + +const useAdminStoreHook = create((set) => ({ + isAdminInfo: false, + setAdminInfo: (value) => set({ isAdminInfo: value }), + fetchAdminInfo: async () => { + try { + const userInfo = await getUserInfo(); + const adminList = await UserApi.findPageAdminUser({ userName: userInfo.userName }); + const matched = adminList.content?.filter((a) => a.userName === userInfo.userName) ?? []; + set({ isAdminInfo: matched.length > 0 }); + } catch { + set({ isAdminInfo: false }); + } + }, +})); + +export const useAdminStore = useAdminStoreHook; +export const getAdminStoreState = () => useAdminStoreHook.getState(); From c957c2ff8d9664d217e346f5f0c9d3fc4b3a072b Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 26 Feb 2026 15:13:56 +0900 Subject: [PATCH 020/113] =?UTF-8?q?st:=20feat:=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20=ED=95=B4=EB=8D=94=20=EC=83=89=EC=83=81=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC,=20=ED=86=B5=EA=B3=84=EC=9E=85=EB=A0=A5=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20&=20=EA=B2=80=EC=83=89=ED=8F=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(=ED=86=B5=EA=B3=84=EC=9E=85=EB=A0=A5=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EB=8F=84=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=ED=95=9C=20=EC=8B=9C=EB=8A=94=20=ED=96=89?= =?UTF-8?q?=EC=A0=95=EA=B5=AC=EC=97=AD(sggNm)=EC=9D=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/css/DataGrid.css | 35 ++++- apps/st/src/components/module/DataGrid.tsx | 3 + apps/st/src/components/module/SearchForm.tsx | 22 ++- apps/st/src/components/module/TabDataGrid.tsx | 3 +- .../input/SeweragePenetrationRateInp.tsx | 134 +++++++++--------- .../statistics/input/StatsInputStatusInp.tsx | 83 ++++++++--- .../statistics/input/SubmitDailog.tsx | 2 +- apps/st/src/components/type/DataGrid.type.ts | 5 +- .../st/src/components/type/SearchForm.type.ts | 2 + .../column/input/StatsInputStatusInpColumn.ts | 52 +++---- ...atistics-input-sew-supply-rate-service.ts} | 8 +- .../api/statistics-input-status-service.ts | 23 +++ .../api/statistics-management-service.ts | 13 -- .../statistics-input-status-interface.ts | 34 +++++ ...ics-sewage-supply-rate-input-interface.ts} | 3 +- 15 files changed, 289 insertions(+), 133 deletions(-) rename apps/st/src/entities/statistics/api/{statistics-input-service.ts => statistics-input-sew-supply-rate-service.ts} (84%) create mode 100644 apps/st/src/entities/statistics/api/statistics-input-status-service.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-input-status-interface.ts rename apps/st/src/entities/statistics/model/{statistics-input-interface.ts => statistics-sewage-supply-rate-input-interface.ts} (98%) diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 888d9fec1..cd21091f9 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -4,9 +4,24 @@ .rg-grid .rg-header .rg-table .rg-header-group-cell { @apply !bg-[#466995] !text-[#fff]; } - .rg-grid .rg-header .rg-table .rg-header-cell { + .rg-grid .rg-header .rg-table { @apply !bg-[#5987bf] !text-[#fff]; } + + /* 상태바·체크바 헤더 */ + .rg-grid .rg-head .rg-table tr:first-child td:first-child, + .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), + .rg-grid .rg-header .rg-table tr:first-child td:first-child, + .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + @apply !bg-[#5987bf] !text-[#fff]; + } + + .rg-grid .rg-header .rg-table tr td, + .rg-grid .rg-head .rg-table tr td { + border: none; + border-bottom: 1px solid #e5e7eb; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-white; } @@ -40,6 +55,12 @@ .rg-grid .rg-header .rg-table .rg-header-cell { @apply !bg-[#62686d] !text-[#fff]; } + .rg-grid .rg-head .rg-table tr:first-child td:first-child, + .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), + .rg-grid .rg-header .rg-table tr:first-child td:first-child, + .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + @apply !bg-[#62686d] !text-[#fff]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-[#545556] !text-[#fff]; } @@ -52,6 +73,12 @@ .rg-grid .rg-header .rg-table .rg-header-cell { @apply !bg-[#8d8d94] !text-[#fff]; } + .rg-grid .rg-head .rg-table tr:first-child td:first-child, + .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), + .rg-grid .rg-header .rg-table tr:first-child td:first-child, + .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + @apply !bg-[#8d8d94] !text-[#fff]; + } } [data-theme="green"] { @@ -61,5 +88,11 @@ .rg-grid .rg-header .rg-table .rg-header-cell { @apply !bg-[#6bafae] !text-[#fff]; } + .rg-grid .rg-head .rg-table tr:first-child td:first-child, + .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), + .rg-grid .rg-header .rg-table tr:first-child td:first-child, + .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + @apply !bg-[#6bafae] !text-[#fff]; + } } } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 8b614f4b7..539c55cdd 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -449,6 +449,7 @@ const DataGrid = ({ target: "local", fileName: `${menuName}.xlsx`, footer: "hidden", + allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함 done: () => { // 헤더 원복 columns.forEach((col) => { @@ -852,6 +853,7 @@ const DataGrid = ({ editable: col.editable, valueType: col.valueType, dataFormat: col.dataFormat, + ...(col.width != null && { width: col.width }), ...(col.styleName != null && { styleName: col.styleName, }), @@ -1146,6 +1148,7 @@ const DataGrid = ({ {...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })} header={column.header} editable={column.editable} + {...(column.width != null && { width: column.width })} {...(baseStyleName != null && { styleName: baseStyleName })} {...(column.visible === false && { visible: false })} {...(cellTooltipCallback && { renderer: { showTooltip: true } })} diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 91d823ed6..1b2899fd2 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import useSWR from 'swr'; import { @@ -19,6 +19,7 @@ import { useAlertStore } from "@/shared/lib"; import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type"; import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface"; +import { doList } from "../util/column/input/DoList"; const getSidoListFetcher = async (): Promise> => { return statisticsAPi.getSidoList(); @@ -66,8 +67,13 @@ const SearchForm = (props: SearchFormProps = {}) => { bsnmCd: props.defaultValues?.bsnmCd || '', }); + // isStatusMenu면 bsnmCd가 doList에 있을 때만 시군구 조회. 선택 후 옵션 유지를 위해 마지막 목록 ref 사용 + const sggKey: [string, string] | null = + values.sido && values.sido !== "ALL" && (!mergedProps.isStatusMenu || doList.includes(values.bsnmCd ?? "")) + ? ["sgg-list", values.sido] + : null; const { data: sggList } = useSWR>( - values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null, + sggKey, getSggListFetcher, { revalidateOnFocus: false, @@ -87,7 +93,9 @@ const SearchForm = (props: SearchFormProps = {}) => { }, } ); - + const lastSggListRef = useRef([]); + if (sggList?.data) lastSggListRef.current = sggList.data; + const sggOptions = sggList?.data ?? (values.sido && values.sido !== "ALL" ? lastSggListRef.current : []); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -95,6 +103,7 @@ const SearchForm = (props: SearchFormProps = {}) => { }; const handleReset = () => { + lastSggListRef.current = []; const resetValues: SearchFormValues = { sido: '', gugun: '', @@ -109,7 +118,7 @@ const SearchForm = (props: SearchFormProps = {}) => { const handleSidoChange = (value: string) => { const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || ''; - + lastSggListRef.current = []; // 시도 변경 시 이전 시군구 목록 제거(조건 안 맞는 시도로 바꿀 때 옵션 안 남도록) setValues((prev) => ({ ...prev, sido: value, @@ -119,9 +128,10 @@ const SearchForm = (props: SearchFormProps = {}) => { }; const handleSggChange = (value: string) => { + const listForBsnm = sggList?.data ?? lastSggListRef.current; const selectedBsnm = value === 'ALL' ? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '') - : (sggList?.data?.find((v) => v.gugun === value)?.bsnmCd || ''); + : (listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ''); setValues((prev) => ({ ...prev, @@ -169,7 +179,7 @@ const SearchForm = (props: SearchFormProps = {}) => { 전체 - {sggList?.data?.map((sgg) => ( + {sggOptions?.map((sgg) => ( {sgg.gugun} diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index b188ce3fe..aa36287ee 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -419,6 +419,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r target: 'local', fileName: `${menuName}_${tabLabel}.xlsx`, footer: 'hidden', + allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함 done: () => { // 헤더 원복 columns.forEach((col) => { @@ -772,7 +773,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r return (
- + {columnInfo.map((info) => { const tabKey = info.tabKey; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index cf7e52e27..d4eaa39d6 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -13,7 +13,7 @@ import SubmitDailog from "./SubmitDailog"; import { useAlertStore } from "@/shared/lib"; import { useAdminStore } from "@/store/admin-store"; -import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service"; +import { StatisticsInputSewSupplyRateApi } from "@/entities/statistics/api/statistics-input-sew-supply-rate-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SewerageSupplyRateItem, @@ -22,7 +22,7 @@ import type { SewerageSupplyRateVerificationResult, SewerageSupplyRateSaveRequest, SewerageSupplyRateStatusInfo, -} from "@/entities/statistics/model/statistics-input-interface"; +} from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; @@ -101,7 +101,7 @@ const SeweragePenetrationRateInp = () => { const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => { try { setIsLoading(true); - const response = await statisticsInputAPi.getSewerageSupplyRateList(params); + const response = await StatisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params); if (response.status === 'success' && response.data) { const commonInfo = response.data.commonInfo; const statusInfo = response.data.statusInfo; @@ -126,7 +126,7 @@ const SeweragePenetrationRateInp = () => { totalPopl: commonInfo.sewerBzmnTotalPopl, poplSm: commonInfo.sewerBzmnPoplSm, frgnrPoplSm: commonInfo.sewerBzmnFrgnrPoplSm, - refArea: commonInfo.sewerBzmnArea, + refArea: commonInfo.sewerBzmnArea, } : null ); @@ -161,7 +161,7 @@ const SeweragePenetrationRateInp = () => { } try { - const response = await statisticsInputAPi.checkInputPeriod(values.year || ''); + const response = await StatisticsInputSewSupplyRateApi.checkInputPeriod(values.year || ''); if (response.status === 'success') { const checkEnterable = response.data; @@ -199,7 +199,8 @@ const SeweragePenetrationRateInp = () => { const submitFunc = async () => { const isError = rowData.filter((d) => d.verificationResult); if (isError.length > 0) { - alert('검증 오류 데이터가 있습니다.'); + alert(`데이터에 오류가 있습니다. + 수정 후 제출해주세요.`); return; } if (await confirm('제출 하시겠습니까?')) { @@ -276,7 +277,8 @@ const SeweragePenetrationRateInp = () => { locgCnfirmDept: submitInfo.locgCnfirmDept, locgCnfirmPh: submitInfo.locgCnfirmPh, }; - const response = await statisticsInputAPi.submitSewerageSupplyRate(requestData); + + const response = await StatisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); alert('제출되었습니다.'); @@ -294,15 +296,15 @@ const SeweragePenetrationRateInp = () => { 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) => { @@ -316,10 +318,10 @@ const SeweragePenetrationRateInp = () => { 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); @@ -370,8 +372,12 @@ const SeweragePenetrationRateInp = () => { sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계 }; - const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData); - + if (isAdminInfo) { + requestData.superAdmin = 'superAdmin'; + } + + const response = await StatisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); + if (response.status === 'success') { fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); alert('저장되었습니다.'); @@ -390,57 +396,57 @@ const SeweragePenetrationRateInp = () => { return ( <> -
- - {rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && ( -
-
- {isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && ( - <> - - getLastYearDataFunc(searchValues ?? {})} /> - - - )} - {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && } +
+ + {rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && ( +
+
+ {isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && ( + <> + + getLastYearDataFunc(searchValues ?? {})} /> + + + )} + {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && } +
-
- )} - {isLoading &&
-
- -
-
} - 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} - rowStartNum={3} - fixedColumns={1} - rowData={rowData} - setRowData={setRowData} - statusInfo={statusInfo} - getGridDataRef={getGridDataRef} - cellStyleCallback={({ row, fieldName }) => { - const verification = (row as GridRowWithVerification).verificationResult; - if (!verification) return; - const errors = verification[fieldName]; - if (Array.isArray(errors) && errors.length > 0) { - return 'rg-cell-verify-error'; - } - }} - cellTooltipCallback={({ row, fieldName }) => { - const verification = (row as GridRowWithVerification).verificationResult; - if (!verification) return; - const errors = verification[fieldName]; - if (Array.isArray(errors) && errors.length > 0) { - return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n'); - } - }} - /> -
- - + )} + {isLoading &&
+
+ +
+
} + 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} + rowStartNum={3} + fixedColumns={1} + rowData={rowData} + setRowData={setRowData} + statusInfo={statusInfo} + getGridDataRef={getGridDataRef} + cellStyleCallback={({ row, fieldName }) => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return 'rg-cell-verify-error'; + } + }} + cellTooltipCallback={({ row, fieldName }) => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n'); + } + }} + /> +
+ + ); }; diff --git a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx index 46f8620c2..057ba5b88 100644 --- a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx +++ b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx @@ -1,14 +1,59 @@ import { useState } from "react"; import SearchForm from "@/components/module/SearchForm"; -import { DataGrid } from "@rsone/components"; +// import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import ApproveButton from "@/components/button/ApproveButton"; import RejectButton from "@/components/button/RejectButton"; +import { LoadingWithAnimation } from "@common/components"; import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn"; +import { StatisticsInputStatusApi } from "@/entities/statistics/api/statistics-input-status-service"; +import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/statistics-input-status-interface"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { useAlertStore } from "@/shared/lib"; +import { toGridRows } from "@/components/util/grid/gridFunction"; // 하수도 통계 관리 > 00. 통계 입력 현황 const StatsInputStatusInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const handleSearch = async (values: SearchFormValues) => { + if (values.sido === '' || values.sido === 'ALL') { + alert('시도를 선택해주세요.'); + return; + } + + const searchParam: StatsInputStatusListRequestParams = { + ctpvNm: values.sido, + sewerBzmnCd: values.bsnmCd, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, + statsYr: values.year, + }; + + try { + setIsLoading(true); + + const response = await StatisticsInputStatusApi.getStatisticsInputStatusList(searchParam); + + if (response.status === 'success' && response.data) { + setRowData(toGridRows(response.data)); + } else { + alert(response.message); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + setRowData([]); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setRowData([]); + }; const rejectFunc = () => { // 보완 요청 로직 @@ -18,25 +63,31 @@ const StatsInputStatusInp = () => { // 승인 요청 로직 }; - const [rowData, setRowData] = useState[]>([]); - return ( <> - -
-
- - +
+ +
+
+ + +
+ {isLoading &&
+
+ +
+
} +
- ); }; diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx index 14cfa06b0..6d0bbbd12 100644 --- a/apps/st/src/components/statistics/input/SubmitDailog.tsx +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -18,7 +18,7 @@ import { useEffect } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; -import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface"; +import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface"; /** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */ function formatPhoneWithHyphen(value: string): string { diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 0106251d5..d9bf1997c 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -1,6 +1,6 @@ import React from "react"; import { ValueType } from "realgrid"; -import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface"; +import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface"; // RealGrid 타입 정의 /** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */ @@ -11,6 +11,7 @@ export type RealGridColumnInfo = { editable?: boolean; valueType?: import("realgrid").ValueType; dataFormat?: string; + width?: number; styleName?: string; mergeRule?: { criteria: "value" | string }; sortable?: boolean; @@ -132,6 +133,8 @@ export type ColumnConfig = { editable?: boolean; // 셀 편집 가능 여부 /** 그리드에서 컬럼 숨김 (수식용 필드 등) */ visible?: boolean; + /** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */ + width?: number; /** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */ styleName?: string; mergeCells?: boolean; diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index b6fd4211d..f3668b56c 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -18,6 +18,7 @@ export type SearchFormProps = { onReset?: () => void; onValuesChange?: (values: SearchFormValues) => void; defaultValues?: SearchFormValues; + isStatusMenu?: boolean; }; export const defaultSearchFormProps: Required> = { @@ -27,4 +28,5 @@ export const defaultSearchFormProps: Required { const response = await axios.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, @@ -49,4 +49,4 @@ export class StatisticsInputAPiService implements StatisticsInputService { } } -export const statisticsInputAPi = new StatisticsInputAPiService(); \ No newline at end of file +export const StatisticsInputSewSupplyRateApi = new StatisticsInputSewSupplyRateApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/statistics-input-status-service.ts b/apps/st/src/entities/statistics/api/statistics-input-status-service.ts new file mode 100644 index 000000000..5dd094e66 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-input-status-service.ts @@ -0,0 +1,23 @@ +import axios from "axios"; +import { ENDPOINT } from "@/shared/config"; + +import type { + StatisticsInputStatusService, + StatsInputStatusListRequestParams, + StatsInputStatusListApiResponse, +} from "../model/statistics-input-status-interface"; + +export class StatisticsInputStatusApiService implements StatisticsInputStatusService { + public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise { + const response = await axios.get ( + `${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`, + { + params: param + } + ); + + return response.data; + }; +} + +export const StatisticsInputStatusApi = new StatisticsInputStatusApiService(); \ 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 f7a75da29..309a7b2f3 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -2,7 +2,6 @@ import axios from "axios"; import { ENDPOINT } from "@/shared/config"; -import { type CheckInputPeriodResponse } from "../model/statistics-input-interface"; import { type StatsInputStatusReviewApiResponse } from "../model/statistics-input-review-interface"; @@ -46,18 +45,6 @@ export class StatisticsManagementApiService { return response.data; } - /** - * 입력 기간 확인 (공통) - * @param year 연도 - */ - public async checkInputPeriod(year: string | number): Promise { - const response = await axios.get( - `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period`, - { params: { year } } - ); - return response.data; - } - /** * 01. 하수도 보급률 검토 목록 조회 * @param params (sido, gugun, yr, bsnmCd) diff --git a/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts new file mode 100644 index 000000000..47637923f --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts @@ -0,0 +1,34 @@ +import type { ApiResponse } from "./statistics-interface"; + +export interface StatsInputStatusListRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +export interface StatsInputStatusList { + sido?: string; + gugun?: string; + menuCd?: string; + menuNm?: string; + locgStatusNm?: string; + inputRate?: number; + locgCharger?: string; + locgCttpc?: string; + locgDept?: string; + locgJob?: string; + locgCnfirmNm?: string; + locgCnfirmDept?: string; + locgCnfirmJob?: string; + returnOpinionSm?: string; +} + +export type StatsInputStatusListApiResponse = ApiResponse & { + message: string; +} + +export interface StatisticsInputStatusService { + getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise; +} diff --git a/apps/st/src/entities/statistics/model/statistics-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts similarity index 98% rename from apps/st/src/entities/statistics/model/statistics-input-interface.ts rename to apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts index 72b519acc..8ed9340ea 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts @@ -146,6 +146,7 @@ export interface SewerageSupplyRateSaveRequest { locgCnfirmJob?: string; locgCnfirmDept?: string; locgCnfirmPh?: string; + superAdmin?: string; } export interface SewerageSupplyRateSaveData { @@ -162,7 +163,7 @@ export interface CheckInputPeriodResponse { data: boolean; } -export interface StatisticsInputService { +export interface StatisticsInputSewSupplyRateService { getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise; checkInputPeriod(params: string): Promise; saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; From fe3e6de085cea02b0481f6d597ff05cb70b2cc6b Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 4 Mar 2026 15:13:07 +0900 Subject: [PATCH 021/113] =?UTF-8?q?[st]=20feat:=20H-B=20=ED=86=B5=EA=B3=84?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 755 +++++++++--------- .../entities/statistics/api/analysis-api.ts | 41 + .../statistics/api/analysis-history-api.ts | 36 + .../model/analysis-history-interface.ts | 49 ++ .../statistics/model/analysis-interface.ts | 64 ++ apps/st/src/shared/config/api.ts | 2 + 6 files changed, 582 insertions(+), 365 deletions(-) create mode 100644 apps/st/src/entities/statistics/api/analysis-api.ts create mode 100644 apps/st/src/entities/statistics/api/analysis-history-api.ts create mode 100644 apps/st/src/entities/statistics/model/analysis-history-interface.ts create mode 100644 apps/st/src/entities/statistics/model/analysis-interface.ts diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 81fa83b2a..3fa0d990b 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -1,115 +1,343 @@ "use client"; -import { useState } from "react"; -// import { DataGrid } from "@rsone/components"; +import { useMemo, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import DataGrid from "@/components/module/DataGrid"; -import SearchForm from "@/components/module/SearchForm"; import { Button, + Checkbox, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, Input, Label, + RadioGroup, + RadioGroupItem, Select, - SelectTrigger, SelectContent, SelectItem, + SelectTrigger, SelectValue, - Checkbox, } from "@common/components"; import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column"; -import { verificationHistoryMockData } from "@/entities/statistics/model/verification-history-mock"; +import { analysisApi } from "@/entities/statistics/api/analysis-api"; +import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; +import type { + AnalysisRequestPayload, + AnalysisResponseDto, +} from "@/entities/statistics/model/analysis-interface"; +import type { + StAnlsMstResponse, + StAnlsOutlierListResponse, +} from "@/entities/statistics/model/analysis-history-interface"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib"; +import { CodeApi } from "@/entities/code"; -interface VerificationFormValues { +type VerifyType = "ALL" | "SELECT"; + +interface VerificationFormState { mu: string; percentile: string; a: string; c: string; verificationYear: string; - checkboxOptions: { - sidoDistribution: boolean; - countyDistribution: boolean; - countyDistributionUserManagement: boolean; - lowerDistribution: boolean; - sidoServiceAvailability: boolean; - individualService: boolean; - legalServiceUsage: boolean; - excessUse: boolean; - lowerOrgStatistics: boolean; - studentStatistics: boolean; - developmentStatistics: boolean; - lowerManagementEnterprise: boolean; +} + +interface OutlierDialogState { + open: boolean; + anlsSn: number | null; +} + +function getVerificationYearOptions(): string[] { + const currentYear = new Date().getFullYear(); + const startYear = 2017; + const endYear = currentYear - 1; + if (endYear < startYear) return []; + return Array.from({ length: endYear - startYear + 1 }, (_, index) => + String(startYear + index), + ); +} + +function toNumber(value: string): number { + const parsed = Number(value); + return Number.isNaN(parsed) ? 0 : parsed; +} + +function mapHistoryToRow( + item: StAnlsMstResponse, +): Record { + const executedAt = item.anlsBgngDt ?? ""; + const endedAt = item.anlsEndDt ?? ""; + const verificationYear = + executedAt && executedAt.length >= 4 ? executedAt.slice(0, 4) : ""; + + return { + executedAt, + progressStatus: item.anlsSttsNm ?? "", + endedAt, + verificationYear, + mu: 0, + percentile: 0, + a: 0, + c: 0, + target: "", + normalCount: item.nmlDataNocs ?? 0, + excludedCount: item.exclDataNocs ?? 0, + abnormalCount: item.otlrDataNocs ?? 0, + anlsSn: item.anlsSn, }; } const HbStatisticsVerification = () => { - const [verificationHistoryRowData, setVerificationHistoryRowData] = - useState[]>( - verificationHistoryMockData, - ); + const { setMessage: alert } = useAlertStore((state) => state); + const queryClient = useQueryClient(); - const [formValues, setFormValues] = useState({ + const yearOptions = useMemo(() => getVerificationYearOptions(), []); + const defaultYear: string = + yearOptions.length > 0 + ? (yearOptions[yearOptions.length - 1] ?? "") + : ""; + + const [verifyType, setVerifyType] = useState("ALL"); + const [formState, setFormState] = useState({ mu: "0", percentile: "0", a: "0", c: "0", - verificationYear: "2025", - checkboxOptions: { - sidoDistribution: false, - countyDistribution: false, - countyDistributionUserManagement: false, - lowerDistribution: false, - sidoServiceAvailability: false, - individualService: false, - legalServiceUsage: false, - excessUse: false, - lowerOrgStatistics: false, - studentStatistics: false, - developmentStatistics: false, - lowerManagementEnterprise: false, + verificationYear: defaultYear, + }); + const [selectedTargets, setSelectedTargets] = useState([]); + const [outlierDialog, setOutlierDialog] = useState({ + open: false, + anlsSn: null, + }); + + const { data: statsItemList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"], + queryFn: async () => { + const response = await CodeApi.getCode("STITEM"); + return response; }, }); - const handleCheckboxChange = ( - key: keyof VerificationFormValues["checkboxOptions"], - ) => { - setFormValues((prev) => ({ - ...prev, - checkboxOptions: { - ...prev.checkboxOptions, - [key]: !prev.checkboxOptions[key], - }, - })); - }; + const { data: analysisHistory } = useQuery({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], + queryFn: async () => { + const response = await analysisHistoryApi.getAnalysisHistoryList({ + page: 0, + size: 10, + }); + return response; + }, + }); - const handleInputChange = ( - key: keyof Omit, + const historyRowData: Record[] = useMemo( + () => + (analysisHistory?.content ?? []).map((item) => + mapHistoryToRow(item as StAnlsMstResponse), + ), + [analysisHistory], + ); + + const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, + "outlier", + outlierDialog.anlsSn, + ], + queryFn: async () => { + if (!outlierDialog.anlsSn) return []; + const response = await analysisHistoryApi.getOutlierList( + outlierDialog.anlsSn, + ); + return response; + }, + enabled: outlierDialog.open && !!outlierDialog.anlsSn, + }); + + const analysisMutation = useMutation< + AnalysisResponseDto, + Error, + AnalysisRequestPayload + >({ + mutationFn: async (payload) => analysisApi.analyze(payload), + onSuccess: () => { + alert("검증을 요청했습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], + }); + }, + onError: () => { + alert("검증 실행 중 오류가 발생했습니다."); + }, + }); + + function handleFormChange( + key: Key, value: string, - ) => { - setFormValues((prev) => ({ + ) { + setFormState((prev) => ({ ...prev, [key]: value, })); - }; + } - const handleSubmit = () => { - console.log("검증 실행:", formValues); - }; + function handleTargetToggle(statsCd: string) { + setSelectedTargets((prev) => + prev.includes(statsCd) + ? prev.filter((cd) => cd !== statsCd) + : [...prev, statsCd], + ); + } - const handleReset = () => { - console.log("초기화"); - }; + async function handleSubmit() { + if (!formState.verificationYear) { + alert("검증년도를 선택해 주세요."); + return; + } + + if (verifyType === "SELECT" && selectedTargets.length === 0) { + alert("검증 대상을 1개 이상 선택해 주세요."); + return; + } + + const yearNew = toNumber(formState.verificationYear); + const payload: AnalysisRequestPayload = { + dataset: + verifyType === "ALL" + ? "ALL" + : selectedTargets.length > 0 + ? selectedTargets.join(",") + : undefined, + yearNew, + yearOld: yearNew - 1, + mu: toNumber(formState.mu), + percentile: toNumber(formState.percentile), + a: toNumber(formState.a), + c: toNumber(formState.c), + vrfcMthdCd: "HB", + allVerifyYn: verifyType === "ALL" ? "Y" : "N", + }; + + analysisMutation.mutate(payload); + } + + function handleReset() { + setFormState({ + mu: "0", + percentile: "0", + a: "0", + c: "0", + verificationYear: defaultYear, + }); + setSelectedTargets([]); + } + + function handleHistoryRowClick(row: Record) { + const abnormalCount = Number(row.abnormalCount ?? 0); + const anlsSn = Number(row.anlsSn ?? 0); + if (!anlsSn || abnormalCount <= 0) return; + setOutlierDialog({ open: true, anlsSn }); + } + + function closeOutlierDialog() { + setOutlierDialog({ open: false, anlsSn: null }); + } return ( -
-
- {/* 통계 검증 섹션 */} -
-

- 통계 검증 -

+ <> +
+ {/* 검증 종류 선택 */} +
+
+ + 검증 방식 + + + setVerifyType(value as VerifyType) + } + className="flex items-center gap-4" + > + + + +
+
+ + +
+
+ {/* //검증 종류 선택 */} -
- {/* 왼쪽: 검증 변수 */} -
+ {verifyType === "SELECT" && ( +
+ {/* 검증 대상 */} +
+

+ 검증 대상 +

+ +
+ {verifyType === "SELECT" && + statsItemList && + statsItemList.length > 0 && ( +
+ {statsItemList.map((item) => ( +
+ + handleTargetToggle( + item.lwpoCd, + ) + } + /> + +
+ ))} +
+ )} +
+
+ {/* //검증 대상 */} + + {/* 검증 변수 */} +

검증 변수

@@ -119,11 +347,11 @@ const HbStatisticsVerification = () => { - handleInputChange( + value={formState.mu} + onChange={(event) => + handleFormChange( "mu", - e.target.value, + event.target.value, ) } className="flex-1" @@ -136,11 +364,11 @@ const HbStatisticsVerification = () => { - handleInputChange( + value={formState.percentile} + onChange={(event) => + handleFormChange( "percentile", - e.target.value, + event.target.value, ) } className="flex-1" @@ -151,11 +379,11 @@ const HbStatisticsVerification = () => { - handleInputChange( + value={formState.a} + onChange={(event) => + handleFormChange( "a", - e.target.value, + event.target.value, ) } className="flex-1" @@ -166,11 +394,11 @@ const HbStatisticsVerification = () => { - handleInputChange( + value={formState.c} + onChange={(event) => + handleFormChange( "c", - e.target.value, + event.target.value, ) } className="flex-1" @@ -182,9 +410,9 @@ const HbStatisticsVerification = () => { 검증 년도
- - {/* 오른쪽: 검증 대상 */} -
-

- 검증 대상 -

- -
-
-
- - handleCheckboxChange( - "sidoDistribution", - ) - } - /> - -
- -
- - handleCheckboxChange( - "countyDistribution", - ) - } - /> - -
- -
- - handleCheckboxChange( - "countyDistributionUserManagement", - ) - } - /> - -
- -
- - handleCheckboxChange( - "lowerDistribution", - ) - } - /> - -
-
- -
-
- - handleCheckboxChange( - "sidoServiceAvailability", - ) - } - /> - -
- -
- - handleCheckboxChange( - "individualService", - ) - } - /> - -
- -
- - handleCheckboxChange( - "sidoServiceAvailability", - ) - } - /> - -
- -
- - handleCheckboxChange( - "individualService", - ) - } - /> - -
-
- -
-
- - handleCheckboxChange( - "lowerOrgStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "studentStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "lowerOrgStatistics", - ) - } - /> - -
- -
- - handleCheckboxChange( - "studentStatistics", - ) - } - /> - -
-
- -
-
- - handleCheckboxChange( - "lowerManagementEnterprise", - ) - } - /> - -
-
-
-
+ {/* //검증 변수 */}
-
- - {/* 버튼 */} -
- - -
- - {/* 검증 이력 섹션 */} -
-

- 검증 이력 -

- -
+ )}
-
+ +
+

+ 검증 이력 +

+ {}} + isExcelImport={false} + isExcelExport={false} + showHeaderCheckbox={false} + onRowClick={handleHistoryRowClick} + cellStyleCallback={({ row, fieldName }) => { + if (fieldName === "abnormalCount") { + const abnormal = Number(row.abnormalCount ?? 0); + if (abnormal > 0) return "rg-cell-verify-error"; + } + return ""; + }} + /> +
+ + + + + 이상건수 상세 + + {isLoadingOutlier && ( +

+ 이상치 데이터를 불러오는 중입니다. +

+ )} + {!isLoadingOutlier && (outlierList?.length ?? 0) === 0 && ( +

+ 표시할 이상치 데이터가 없습니다. +

+ )} + {!isLoadingOutlier && (outlierList?.length ?? 0) > 0 && ( +
+ {outlierList?.map( + (item: StAnlsOutlierListResponse) => ( +
+
+ + {item.vrfcTrgtNm} + + + 이상건수:{" "} + {item.otlrDataNocs ?? 0} + +
+
+ ), + )} +
+ )} +
+
+ ); }; diff --git a/apps/st/src/entities/statistics/api/analysis-api.ts b/apps/st/src/entities/statistics/api/analysis-api.ts new file mode 100644 index 000000000..0b607b299 --- /dev/null +++ b/apps/st/src/entities/statistics/api/analysis-api.ts @@ -0,0 +1,41 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + AnalysisRequestPayload, + AnalysisResponseDto, + JobStatusDto, + AnalysisResultDto, +} from "../model/analysis-interface"; + +export class AnalysisApiService { + public async analyze( + payload: AnalysisRequestPayload, + ): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`, + payload, + ); + + return response.data; + } + + public async getJobStatus(jobId: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`, + ); + + return response.data; + } + + public async getResult(jobId: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`, + ); + + return response.data; + } +} + +export const analysisApi = new AnalysisApiService(); + diff --git a/apps/st/src/entities/statistics/api/analysis-history-api.ts b/apps/st/src/entities/statistics/api/analysis-history-api.ts new file mode 100644 index 000000000..56375bf5f --- /dev/null +++ b/apps/st/src/entities/statistics/api/analysis-history-api.ts @@ -0,0 +1,36 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + StAnlsMstListApiResponse, + StAnlsMstSearchParams, + StAnlsOutlierListResponse, +} from "../model/analysis-history-interface"; + +export class AnalysisHistoryApiService { + public async getAnalysisHistoryList( + params: StAnlsMstSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, + { + params, + }, + ); + + return response.data; + } + + public async getOutlierList( + anlsSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, + ); + + return response.data; + } +} + +export const analysisHistoryApi = new AnalysisHistoryApiService(); + diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts new file mode 100644 index 000000000..c37d4fe5c --- /dev/null +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -0,0 +1,49 @@ +import type { PageableList } from "@/shared/model"; + +export interface StAnlsMstResponse { + anlsSn: number; + anlsBgngDt: string; + anlsEndDt: string | null; + anlsSttsCd: string; + anlsSttsNm: string; + nmlDataNocs: number; + exclDataNocs: number; + otlrDataNocs: number; + jobUuid: string; + regDt: string; + rgtr: string; + rgtrNm: string; +} + +export interface StAnlsMstSearchParams { + anlsSttsCd?: string; + jobUuid?: string; + page?: number; + size?: number; + sort?: string; +} + +export interface StAnlsOutlierDataResponse { + rsltSn: number; + vrfcTrgtSn: number; + admdstCd: string; + sido: string; + gugun: string; + adstDong: string; + addr: string; + cryrVl: number; + otlrTrsfRt: number; +} + +export interface StAnlsOutlierListResponse { + anlsSn: number; + vrfcTrgtSn: number; + vrfcTrgtNm: string; + nmlDataNocs: number; + exclDataNocs: number; + otlrDataNocs: number; + outlierDataList: StAnlsOutlierDataResponse[]; +} + +export type StAnlsMstListApiResponse = PageableList; + diff --git a/apps/st/src/entities/statistics/model/analysis-interface.ts b/apps/st/src/entities/statistics/model/analysis-interface.ts new file mode 100644 index 000000000..96ef8a6cc --- /dev/null +++ b/apps/st/src/entities/statistics/model/analysis-interface.ts @@ -0,0 +1,64 @@ +import type { PageableList } from "@/shared/model"; + +export interface AnalysisRequestPayload { + dataset?: string; + yearOld: number; + yearNew: number; + mu: number; + a: number; + c: number; + percentile: number; + sido?: string; + sigungu?: string; + vrfcMthdCd?: string; + allVerifyYn?: "Y" | "N"; +} + +export interface AnalysisResponseDto { + jobId: string; + status: string; + message: string; + queuePosition: number; + createdAt: string; + anlsSn: number; +} + +export interface JobStatusDto { + jobId: string; + status: string; + message: string; + queuePosition: number; + progress: number; + startedAt: string | null; + completedAt: string | null; + error: string | null; + anlsSn: number | null; + processingTimeMs: number | null; +} + +export interface AnalysisSummaryDto { + // 요약 필드는 Python 분석 서비스 스키마에 따라 확장 + [key: string]: unknown; +} + +export interface OutlierRecordDto { + // 상세 구조는 Python 분석 서비스 응답에 맞춰 필요 시 확장 + [key: string]: unknown; +} + +export interface AnalysisResultDto { + jobId: string; + status: string; + createdAt: string; + completedAt: string | null; + summary: AnalysisSummaryDto | null; + multiSummaries: Record | null; + outliers: OutlierRecordDto[]; + results: OutlierRecordDto[]; + totalCount: number; + cached: boolean; + processingTimeMs: number; + anlsSn: number; +} + +export type AnalysisHistoryListApiResponse = PageableList; diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 1b9cf74ec..cc38d4c34 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -72,6 +72,8 @@ export const ENDPOINT = { }, STATVERIFY_SERVICE: { VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng", + ANALYSIS: "/statverify-service/api/v1/stat-verify/analysis", + ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst", }, }; From c72b2c3f5851e747bd881f7ed75bc2700d81c14c Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 4 Mar 2026 16:22:21 +0900 Subject: [PATCH 022/113] =?UTF-8?q?[st]=20feat:=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=EA=B1=B4=EC=88=98=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 88 +++++++++++++++---- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 3fa0d990b..67787c264 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -12,6 +12,7 @@ import { DialogTitle, Input, Label, + LoadingWithAnimation, RadioGroup, RadioGroupItem, Select, @@ -19,6 +20,12 @@ import { SelectItem, SelectTrigger, SelectValue, + Table, + TableBody, + TableHead, + TableHeader, + TableRow, + TableCell, } from "@common/components"; import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column"; import { analysisApi } from "@/entities/statistics/api/analysis-api"; @@ -468,9 +475,11 @@ const HbStatisticsVerification = () => { 이상건수 상세 {isLoadingOutlier && ( -

- 이상치 데이터를 불러오는 중입니다. -

+
+
+ +
+
)} {!isLoadingOutlier && (outlierList?.length ?? 0) === 0 && (

@@ -478,23 +487,70 @@ const HbStatisticsVerification = () => {

)} {!isLoadingOutlier && (outlierList?.length ?? 0) > 0 && ( -
+
{outlierList?.map( (item: StAnlsOutlierListResponse) => ( -
-
- - {item.vrfcTrgtNm} - - - 이상건수:{" "} - {item.otlrDataNocs ?? 0} - -
-
+ + + + {item.vrfcTrgtNm} + + + + + {Array.from({ + length: Math.ceil( + item.outlierDataList + .length / 4, + ), + }).map((_, rowIdx) => ( + + {item.outlierDataList + .slice( + rowIdx * 4, + rowIdx * 4 + 4, + ) + .map((data) => ( + + {`${data.sido ?? ""}${data.adstDong ? ` ${data.adstDong}` : ""}`.trim() || + "-"} + + ))} + {/* 빈 셀 추가 */} + {item.outlierDataList.slice( + rowIdx * 4, + rowIdx * 4 + 4, + ).length < 4 && + Array.from({ + length: + 4 - + item.outlierDataList.slice( + rowIdx * 4, + rowIdx * 4 + + 4, + ).length, + }).map((_, idx) => ( + + ))} + + ))} + + ), )}
From 64fcc423b50a0b23b68eaf5374c5517e4c04b908 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 4 Mar 2026 17:17:40 +0900 Subject: [PATCH 023/113] =?UTF-8?q?[st]=20chore:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 48 ++-------- .../model/verification-history-column.ts | 92 +++++-------------- 2 files changed, 31 insertions(+), 109 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 67787c264..2a356aa43 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -34,10 +34,7 @@ import type { AnalysisRequestPayload, AnalysisResponseDto, } from "@/entities/statistics/model/analysis-interface"; -import type { - StAnlsMstResponse, - StAnlsOutlierListResponse, -} from "@/entities/statistics/model/analysis-history-interface"; +import type { StAnlsOutlierListResponse } from "@/entities/statistics/model/analysis-history-interface"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; import { CodeApi } from "@/entities/code"; @@ -72,31 +69,6 @@ function toNumber(value: string): number { return Number.isNaN(parsed) ? 0 : parsed; } -function mapHistoryToRow( - item: StAnlsMstResponse, -): Record { - const executedAt = item.anlsBgngDt ?? ""; - const endedAt = item.anlsEndDt ?? ""; - const verificationYear = - executedAt && executedAt.length >= 4 ? executedAt.slice(0, 4) : ""; - - return { - executedAt, - progressStatus: item.anlsSttsNm ?? "", - endedAt, - verificationYear, - mu: 0, - percentile: 0, - a: 0, - c: 0, - target: "", - normalCount: item.nmlDataNocs ?? 0, - excludedCount: item.exclDataNocs ?? 0, - abnormalCount: item.otlrDataNocs ?? 0, - anlsSn: item.anlsSn, - }; -} - const HbStatisticsVerification = () => { const { setMessage: alert } = useAlertStore((state) => state); const queryClient = useQueryClient(); @@ -140,13 +112,11 @@ const HbStatisticsVerification = () => { }, }); - const historyRowData: Record[] = useMemo( - () => - (analysisHistory?.content ?? []).map((item) => - mapHistoryToRow(item as StAnlsMstResponse), - ), - [analysisHistory], - ); + const historyRowData = + (analysisHistory?.content ?? []) as unknown as Record< + string, + string | number + >[]; const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({ queryKey: [ @@ -243,7 +213,7 @@ const HbStatisticsVerification = () => { } function handleHistoryRowClick(row: Record) { - const abnormalCount = Number(row.abnormalCount ?? 0); + const abnormalCount = Number(row.otlrDataNocs ?? 0); const anlsSn = Number(row.anlsSn ?? 0); if (!anlsSn || abnormalCount <= 0) return; setOutlierDialog({ open: true, anlsSn }); @@ -460,8 +430,8 @@ const HbStatisticsVerification = () => { showHeaderCheckbox={false} onRowClick={handleHistoryRowClick} cellStyleCallback={({ row, fieldName }) => { - if (fieldName === "abnormalCount") { - const abnormal = Number(row.abnormalCount ?? 0); + if (fieldName === "otlrDataNocs") { + const abnormal = Number(row.otlrDataNocs ?? 0); if (abnormal > 0) return "rg-cell-verify-error"; } return ""; diff --git a/apps/st/src/entities/statistics/model/verification-history-column.ts b/apps/st/src/entities/statistics/model/verification-history-column.ts index aea9b7f4f..e6c635a54 100644 --- a/apps/st/src/entities/statistics/model/verification-history-column.ts +++ b/apps/st/src/entities/statistics/model/verification-history-column.ts @@ -5,79 +5,36 @@ import type { ColumnInfo } from "@rsone/components"; export const verificationHistoryColumnInfo: ColumnInfo = { columns: [ { - name: "executedAt", - fieldName: "executedAt", + name: "anlsBgngDt", + fieldName: "anlsBgngDt", header: { text: "검증 실행 일시" }, editable: false, sortdisable: false, }, { - name: "progressStatus", - fieldName: "progressStatus", + name: "anlsSttsNm", + fieldName: "anlsSttsNm", header: { text: "진행 상태" }, editable: false, sortdisable: false, }, { - name: "endedAt", - fieldName: "endedAt", + name: "anlsEndDt", + fieldName: "anlsEndDt", header: { text: "종료 일시" }, editable: false, sortdisable: false, }, { - name: "verificationYear", - fieldName: "verificationYear", - header: { text: "검증년도" }, + name: "rgtrNm", + fieldName: "rgtrNm", + header: { text: "검증자" }, editable: false, sortdisable: false, }, { - name: "mu", - fieldName: "mu", - header: { text: "μ" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.###", - }, - { - name: "percentile", - fieldName: "percentile", - header: { text: "Percentile" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.###", - }, - { - name: "a", - fieldName: "a", - header: { text: "A" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.###", - }, - { - name: "c", - fieldName: "c", - header: { text: "C" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.###", - }, - { - name: "target", - fieldName: "target", - header: { text: "검증 대상" }, - editable: false, - sortdisable: false, - }, - { - name: "normalCount", - fieldName: "normalCount", + name: "nmlDataNocs", + fieldName: "nmlDataNocs", header: { text: "정상건수" }, editable: false, sortdisable: false, @@ -85,8 +42,8 @@ export const verificationHistoryColumnInfo: ColumnInfo = { dataFormat: "#,##0", }, { - name: "excludedCount", - fieldName: "excludedCount", + name: "exclDataNocs", + fieldName: "exclDataNocs", header: { text: "제외건수" }, editable: false, sortdisable: false, @@ -94,8 +51,8 @@ export const verificationHistoryColumnInfo: ColumnInfo = { dataFormat: "#,##0", }, { - name: "abnormalCount", - fieldName: "abnormalCount", + name: "otlrDataNocs", + fieldName: "otlrDataNocs", header: { text: "이상건수" }, editable: false, sortdisable: false, @@ -104,17 +61,12 @@ export const verificationHistoryColumnInfo: ColumnInfo = { }, ], columnLayout: [ - "executedAt", - "progressStatus", - "endedAt", - "verificationYear", - "mu", - "percentile", - "a", - "c", - "target", - "normalCount", - "excludedCount", - "abnormalCount", + "anlsBgngDt", + "anlsSttsNm", + "anlsEndDt", + "rgtrNm", + "nmlDataNocs", + "exclDataNocs", + "otlrDataNocs", ], }; From 63afdfc726c3e8410fa2507ec38ddfd61c847c70 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 5 Mar 2026 10:10:56 +0900 Subject: [PATCH 024/113] =?UTF-8?q?st:=20fix:=20db=20=ED=91=9C=EC=A4=80?= =?UTF-8?q?=ED=99=94=20=EC=9E=91=EC=97=85=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?interface=20=EB=B0=8F=20type=20=EC=88=98=EC=A0=95,=20middleware?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/middleware.ts | 33 ++++++------ .../components/button/CancelSubmitButton.tsx | 16 ++++++ apps/st/src/components/module/DataGrid.tsx | 18 +++---- apps/st/src/components/module/SearchForm.tsx | 4 +- .../input/SeweragePenetrationRateInp.tsx | 33 ++++++------ .../statistics/input/StatsInputStatusInp.tsx | 2 +- .../statistics/input/SubmitDailog.tsx | 50 +++++++++---------- .../statistics/management/LogHistory.tsx | 7 +++ .../st/src/components/type/SearchForm.type.ts | 2 - .../column/input/StatsInputStatusInpColumn.ts | 38 +++++++------- ...tatistics-input-sew-supply-rate-service.ts | 24 ++++----- .../api/statistics-input-status-service.ts | 13 ++--- .../api/statistics-management-service.ts | 24 ++++----- .../statistics/api/statistics-service.ts | 19 +++---- .../statistics-input-status-interface.ts | 16 +++--- .../statistics-input-submit-info-schema.ts | 16 +++--- ...tics-sewage-supply-rate-input-interface.ts | 44 ++++++++-------- apps/st/src/shared/api/client.ts | 11 +++- apps/st/src/shared/config/api.ts | 2 + 19 files changed, 197 insertions(+), 175 deletions(-) create mode 100644 apps/st/src/components/button/CancelSubmitButton.tsx create mode 100644 apps/st/src/components/statistics/management/LogHistory.tsx diff --git a/apps/st/middleware.ts b/apps/st/middleware.ts index 8b1ecb501..9a2a48cb1 100644 --- a/apps/st/middleware.ts +++ b/apps/st/middleware.ts @@ -12,10 +12,10 @@ import { const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; -const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; - const stServiceUrl = ST_SERVICE_URL; +const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; + // CSP 헤더 설정 함수 (img-src만) function addSecurityHeaders(response: NextResponse): NextResponse { response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;"); @@ -28,41 +28,46 @@ export function middleware(req: NextRequest) { const logMessages: string[] = [ `[Middleware Request] ${new Date().toISOString()}`, - `[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`, + `[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}\n ST Service URL: ${stServiceUrl}`, `[Request]\n Original Pathname: ${pathname}`, ]; - if (pathname.startsWith("/st-management-service") || pathname.startsWith("/st-input-service")) { + // ST Service 처리 (dc의 /server/dc-service 패턴과 동일) + if (pathname.startsWith("/server/st-management-service") || pathname.startsWith("/server/st-input-service")) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - + const queryParams = searchParams.toString(); - const newUrl = `${stServiceUrl}${pathname}${ + const newUrl = `${stServiceUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); - - // 디버깅을 위한 토큰 정보 로그 - logMessages.push(` Token Debug: ${token ? 'EXISTS' : 'MISSING'}`); - logMessages.push(` UserId Debug: ${userId ? 'EXISTS' : 'MISSING'}`); + + logMessages.push(` Token Debug: ${token ? "EXISTS" : "MISSING"}`); + logMessages.push(` UserId Debug: ${userId ? "EXISTS" : "MISSING"}`); logMessages.push(` ST Service Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); console.log(logMessages.join("\n")); - + const response = NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); return addSecurityHeaders(response); } - if (pathname.startsWith("/server")) { + // 일반 Server API 처리 (ST 서비스 제외) + if ( + pathname.startsWith("/server") && + !pathname.startsWith("/server/st-management-service") && + !pathname.startsWith("/server/st-input-service") + ) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - + const queryParams = searchParams.toString(); const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" @@ -138,5 +143,5 @@ export function middleware(req: NextRequest) { } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/st-management-service/:path*", "/st-input-service/:path*", "/"], + matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"], }; diff --git a/apps/st/src/components/button/CancelSubmitButton.tsx b/apps/st/src/components/button/CancelSubmitButton.tsx new file mode 100644 index 000000000..c8b7c510f --- /dev/null +++ b/apps/st/src/components/button/CancelSubmitButton.tsx @@ -0,0 +1,16 @@ +import { Button } from "@common/components"; + +interface CancelSubmitButtonProps { + cnacelSubmitFunc: () => void; +} + +const CancelSubmitButton = ({ cnacelSubmitFunc }: CancelSubmitButtonProps) => { + return ( + + ); +}; + +export default CancelSubmitButton; \ No newline at end of file diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 539c55cdd..cc1c65613 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1039,21 +1039,21 @@ const DataGrid = ({ {statusInfo &&
진행상황: { - statusInfo.locgStatus === null || statusInfo.locgStatus === '0' ? + statusInfo.laeSbmsnSttsCd === null || statusInfo.laeSbmsnSttsCd === '0' ? '미작성' : - statusInfo.locgStatus === '1' ? '작성중' : statusInfo.locgStatus === '2' ? '제출' + statusInfo.laeSbmsnSttsCd === '1' ? '작성중' : statusInfo.laeSbmsnSttsCd === '2' ? '제출' : - statusInfo.locgStatus === '3' ? '승인' : '반려' + statusInfo.laeSbmsnSttsCd === '3' ? '승인' : '반려' } - 제출일: {statusInfo.locgDt - ? (statusInfo.locgDt.length === 8 && /^\d{8}$/.test(statusInfo.locgDt) - ? `${statusInfo.locgDt.slice(0, 4)}-${statusInfo.locgDt.slice(4, 6)}-${statusInfo.locgDt.slice(6, 8)}` - : statusInfo.locgDt) + 제출일: {statusInfo.laeSbmsnYmd + ? (statusInfo.laeSbmsnYmd.length === 8 && /^\d{8}$/.test(statusInfo.laeSbmsnYmd) + ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` + : statusInfo.laeSbmsnYmd) : '미제출'} - 담당자: {statusInfo.locgCharger ? statusInfo.locgCharger : '-'} - 연락처: {statusInfo.locgCttpc ? statusInfo.locgCttpc : '-'} + 담당자: {statusInfo.laePicNm ? statusInfo.laePicNm : '-'} + 연락처: {statusInfo.laePicTelNo ? statusInfo.laePicTelNo : '-'}
} {isExcelImport && ( diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 1b2899fd2..10221d304 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -67,9 +67,9 @@ const SearchForm = (props: SearchFormProps = {}) => { bsnmCd: props.defaultValues?.bsnmCd || '', }); - // isStatusMenu면 bsnmCd가 doList에 있을 때만 시군구 조회. 선택 후 옵션 유지를 위해 마지막 목록 ref 사용 const sggKey: [string, string] | null = - values.sido && values.sido !== "ALL" && (!mergedProps.isStatusMenu || doList.includes(values.bsnmCd ?? "")) + // values.sido && values.sido !== "ALL" && doList.includes(values.bsnmCd ?? "") + values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null; const { data: sggList } = useSWR>( diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index d4eaa39d6..feab264cb 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -7,6 +7,7 @@ import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import { LoadingWithAnimation } from "@common/components"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import SubmitDailog from "./SubmitDailog"; @@ -123,16 +124,16 @@ const SeweragePenetrationRateInp = () => { setFooterFromApi( commonInfo ? { - totalPopl: commonInfo.sewerBzmnTotalPopl, - poplSm: commonInfo.sewerBzmnPoplSm, - frgnrPoplSm: commonInfo.sewerBzmnFrgnrPoplSm, + totalPopl: commonInfo.sewerBzmnWholPpltnCnt, + poplSm: commonInfo.sewerBzmnDomplWholPpltnCnt, + frgnrPoplSm: commonInfo.sewerBzmnFrgnrWholPpltnCnt, refArea: commonInfo.sewerBzmnArea, } : null ); - if (statusInfo && statusInfo.locgStatus) { - if (['2', '3'].includes(statusInfo.locgStatus)) { + if (statusInfo && statusInfo.laeSbmsnSttsCd) { + if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) { setIsEnterable(false); } else { setIsEnterable(isEnterable); @@ -267,15 +268,15 @@ const SeweragePenetrationRateInp = () => { statsYr: searchValues?.year ?? '', userId: userInfo.userUnqId, submitList: convertedData, - sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm, - locgCharger: submitInfo.locgCharger, - locgJob: submitInfo.locgJob, - locgDept: submitInfo.locgDept, - locgCttpc: submitInfo.locgCttpc, - locgCnfirmNm: submitInfo.locgCnfirmNm, - locgCnfirmJob: submitInfo.locgCnfirmJob, - locgCnfirmDept: submitInfo.locgCnfirmDept, - locgCnfirmPh: submitInfo.locgCnfirmPh, + sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.frgnrPoplSm, + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, }; const response = await StatisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); @@ -369,7 +370,7 @@ const SeweragePenetrationRateInp = () => { statsYr: searchValues?.year ?? '', // 통계 연도 userId: userInfo.userUnqId, // 사용자 saveList: convertedData, // 저장 데이터 - sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계 + sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계 }; if (isAdminInfo) { @@ -406,8 +407,10 @@ const SeweragePenetrationRateInp = () => { getLastYearDataFunc(searchValues ?? {})} /> + )} + {statusInfo?.laeSbmsnSttsCd === '2' && console.log('test')} />} {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && }
diff --git a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx index 057ba5b88..00641e312 100644 --- a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx +++ b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx @@ -66,7 +66,7 @@ const StatsInputStatusInp = () => { return ( <>
- +
diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx index 6d0bbbd12..b286117b7 100644 --- a/apps/st/src/components/statistics/input/SubmitDailog.tsx +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -37,7 +37,7 @@ function formatPhoneWithHyphen(value: string): string { return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`; } -const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["locgCttpc", "locgCnfirmPh"]; +const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["laePicTelNo", "laeIdfrTelNo"]; interface SubmitDialogProps { open: boolean; @@ -48,26 +48,26 @@ interface SubmitDialogProps { } const defaultValues: StatisticsInputSubmitInfoFormData = { - locgCharger: "", - locgJob: "", - locgDept: "", - locgCttpc: "", - locgCnfirmNm: "", - locgCnfirmJob: "", - locgCnfirmDept: "", - locgCnfirmPh: "", + laePicNm: "", + laeKbrdrJbgdNm: "", + laeKbrdrDeptNm: "", + laePicTelNo: "", + laeIdfrNm: "", + laeIdfrJbgdNm: "", + laeIdfrDeptNm: "", + laeIdfrTelNo: "", }; function statusInfoToFormData(info: SewerageSupplyRateStatusInfo): StatisticsInputSubmitInfoFormData { return { - locgCharger: info.locgCharger ?? "", - locgJob: info.locgJob ?? "", - locgDept: info.locgDept ?? "", - locgCttpc: info.locgCttpc ?? "", - locgCnfirmNm: info.locgCnfirmNm ?? "", - locgCnfirmJob: info.locgCnfirmJob ?? "", - locgCnfirmDept: info.locgCnfirmDept ?? "", - locgCnfirmPh: info.locgCnfirmPh ?? "", + laePicNm: info.laePicNm ?? "", + laeKbrdrJbgdNm: info.laeKbrdrJbgdNm ?? "", + laeKbrdrDeptNm: info.laeKbrdrDeptNm ?? "", + laePicTelNo: info.laePicTelNo ?? "", + laeIdfrNm: info.laeIdfrNm ?? "", + laeIdfrJbgdNm: info.laeIdfrJbgdNm ?? "", + laeIdfrDeptNm: info.laeIdfrDeptNm ?? "", + laeIdfrTelNo: info.laeIdfrTelNo ?? "", }; } @@ -95,14 +95,14 @@ const SubmitDailog = ({ open, onOpenChange, statusInfo, onConfirm }: SubmitDialo } const rows: { name: keyof StatisticsInputSubmitInfoFormData; label: string; placeholder: string }[] = [ - { name: "locgCharger", label: "작성자 명", placeholder: "작성자 명 입력" }, - { name: "locgJob", label: "작성자 직제", placeholder: "작성자 직제 입력" }, - { name: "locgDept", label: "작성자 부서", placeholder: "작성자 부서 입력" }, - { name: "locgCttpc", label: "작성자 연락처", placeholder: "작성자 연락처 입력" }, - { name: "locgCnfirmNm", label: "승인자명", placeholder: "승인자명 입력" }, - { name: "locgCnfirmJob", label: "승인자 직제", placeholder: "승인자 직제 입력" }, - { name: "locgCnfirmDept", label: "승인자 부서", placeholder: "승인자 부서 입력" }, - { name: "locgCnfirmPh", label: "승인자 연락처", placeholder: "승인자 연락처" }, + { name: "laePicNm", label: "작성자 명", placeholder: "작성자 명 입력" }, + { name: "laeKbrdrJbgdNm", label: "작성자 직제", placeholder: "작성자 직제 입력" }, + { name: "laeKbrdrDeptNm", label: "작성자 부서", placeholder: "작성자 부서 입력" }, + { name: "laePicTelNo", label: "작성자 연락처", placeholder: "작성자 연락처 입력" }, + { name: "laeIdfrNm", label: "승인자명", placeholder: "승인자명 입력" }, + { name: "laeIdfrJbgdNm", label: "승인자 직제", placeholder: "승인자 직제 입력" }, + { name: "laeIdfrDeptNm", label: "승인자 부서", placeholder: "승인자 부서 입력" }, + { name: "laeIdfrTelNo", label: "승인자 연락처", placeholder: "승인자 연락처" }, ]; return ( diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx new file mode 100644 index 000000000..5950c1125 --- /dev/null +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -0,0 +1,7 @@ +const LogHistory = () => { + return ( + <> + ); +}; + +export default LogHistory; \ No newline at end of file diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index f3668b56c..b6fd4211d 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -18,7 +18,6 @@ export type SearchFormProps = { onReset?: () => void; onValuesChange?: (values: SearchFormValues) => void; defaultValues?: SearchFormValues; - isStatusMenu?: boolean; }; export const defaultSearchFormProps: Required> = { @@ -28,5 +27,4 @@ export const defaultSearchFormProps: Required 00. 통계 입력 현황 @@ -10,15 +10,15 @@ export const StatsInputStatusInpColumn = { { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false }, { name: 'menuCd', fieldName: 'menuCd', header: { text: '메뉴코드' }, visible: false, editable: false }, { name: 'menuNm', fieldName: 'menuNm', header: { text: '구분' }, editable: false, width: 220 }, - { name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false }, - { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'locgCharger', fieldName: 'locgCharger', header: { text: '작성자' }, editable: false }, - { name: 'locgCttpc', fieldName: 'locgCttpc', header: { text: '작성자연락처' }, editable: false }, - { name: 'locgDept', fieldName: 'locgDept', header: { text: '부서' }, editable: false }, - { name: 'locgJob', fieldName: 'locgJob', header: { text: '직제' }, editable: false }, - { name: 'locgCnfirmNm', fieldName: 'locgCnfirmNm', header: { text: '승인자' }, editable: false }, - { name: 'locgCnfirmDept', fieldName: 'locgCnfirmDept', header: { text: '부서' }, editable: false }, - { name: 'locgCnfirmJob', fieldName: 'locgCnfirmJob', header: { text: '직제' }, editable: false }, + { name: 'laeSbmsnSttsCdNm', fieldName: 'laeSbmsnSttsCdNm', header: { text: '진행상황' }, editable: false }, + // { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false }, + { name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false }, + { name: 'laeKbrdrDeptNm', fieldName: 'laeKbrdrDeptNm', header: { text: '부서' }, editable: false }, + { name: 'laeKbrdrJbgdNm', fieldName: 'laeKbrdrJbgdNm', header: { text: '직제' }, editable: false }, + { name: 'laeIdfrNm', fieldName: 'laeIdfrNm', header: { text: '승인자' }, editable: false }, + { name: 'laeIdfrDeptNm', fieldName: 'laeIdfrDeptNm', header: { text: '부서' }, editable: false }, + { name: 'laeIdfrJbgdNm', fieldName: 'laeIdfrJbgdNm', header: { text: '직제' }, editable: false }, { name: 'returnOpinionSm', fieldName: 'returnOpinionSm', header: { text: '반려사유' }, editable: false }, ], @@ -32,15 +32,15 @@ export const StatsInputStatusInpColumn = { hideChildHeaders: false, header: { text: '하수도사업자' }, items: [ - 'locgStatusNm', - 'inputRate', - 'locgCharger', - 'locgCttpc', - 'locgDept', - 'locgJob', - 'locgCnfirmNm', - 'locgCnfirmDept', - 'locgCnfirmJob', + 'laeSbmsnSttsCdNm', + // 'inputRate', + 'laePicNm', + 'laePicTelNo', + 'laeKbrdrDeptNm', + 'laeKbrdrJbgdNm', + 'laeIdfrNm', + 'laeIdfrDeptNm', + 'laeIdfrJbgdNm', 'returnOpinionSm' ], }, diff --git a/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts index dbd9d3fb8..ba6d10874 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts @@ -1,7 +1,7 @@ -import axios from "axios"; import { ENDPOINT } from "@/shared/config"; +import { stClient } from "@/shared/api/client"; -import { +import { type StatisticsInputSewSupplyRateService, type SewerageSupplyRateRequestParams, type SewerageSupplyRateApiResponse, @@ -12,39 +12,33 @@ import { export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSewSupplyRateService { public async getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise { - const response = await axios.get ( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, - { - params: params - } + { params } ); - return response.data; } public async checkInputPeriod(params: string): Promise { - const response = await axios.get ( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period?year=${params}`, ); - return response.data; } public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await axios.post ( + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, - params + params, ); - return response.data; } public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await axios.post ( + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`, - params + params, ); - return response.data; } } diff --git a/apps/st/src/entities/statistics/api/statistics-input-status-service.ts b/apps/st/src/entities/statistics/api/statistics-input-status-service.ts index 5dd094e66..cd3c55a5f 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-status-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-status-service.ts @@ -1,7 +1,7 @@ -import axios from "axios"; import { ENDPOINT } from "@/shared/config"; +import { stClient } from "@/shared/api/client"; -import type { +import type { StatisticsInputStatusService, StatsInputStatusListRequestParams, StatsInputStatusListApiResponse, @@ -9,15 +9,12 @@ import type { export class StatisticsInputStatusApiService implements StatisticsInputStatusService { public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise { - const response = await axios.get ( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`, - { - params: param - } + { params: param }, ); - return response.data; - }; + } } export const StatisticsInputStatusApi = new StatisticsInputStatusApiService(); \ 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 309a7b2f3..1e2a6336d 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -1,5 +1,5 @@ -import axios from "axios"; import { ENDPOINT } from "@/shared/config"; +import { stClient } from "@/shared/api/client"; import { @@ -38,7 +38,7 @@ export class StatisticsManagementApiService { public async getStatsInputStatusList( params: { yr?: string; bsnmCd?: string; sido?: string; gugun?: string } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/input-status`, { params } ); @@ -52,7 +52,7 @@ export class StatisticsManagementApiService { public async getSewerageSupplyRateReviewList( params: SewerageSupplyRateReviewRequestParams ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/supply-rate`, { params } ); @@ -66,7 +66,7 @@ export class StatisticsManagementApiService { public async getSewagePipeReviewRegionalList( params: SewagePipeReviewRequestParams ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/regional-status`, { params } ); @@ -80,7 +80,7 @@ export class StatisticsManagementApiService { public async getSewagePipeFacilityList( params: SewagePipeReviewRequestParams ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/facility-status`, { params } ); @@ -99,7 +99,7 @@ export class StatisticsManagementApiService { gugun?: string; } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/type-status`, { params } ); @@ -118,7 +118,7 @@ export class StatisticsManagementApiService { gugun?: string; } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/diam-status`, { params } ); @@ -132,7 +132,7 @@ export class StatisticsManagementApiService { public async getSewerPipeMtcList( params: SewerPipeMtcRequestParams ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/repair`, { params } ); @@ -151,7 +151,7 @@ export class StatisticsManagementApiService { gugun?: string; } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/dredging`, { params } ); @@ -170,7 +170,7 @@ export class StatisticsManagementApiService { gugun?: string; } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/facilities`, { params } ); @@ -189,7 +189,7 @@ export class StatisticsManagementApiService { gugun?: string; } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/pumping-stations`, { params } ); @@ -209,7 +209,7 @@ export class StatisticsManagementApiService { bscFctNm?: string; // 펌프장명 (시설명) } ): Promise { - const response = await axios.get( + const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/mediation-pump-stations`, { params } ); diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index 8455e68c5..f75163e5c 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -1,33 +1,26 @@ -import axios from "axios"; import { ENDPOINT } from "@/shared/config"; +import { stClient } from "@/shared/api/client"; import { type StatisticsService, type SearchCondition, type ApiResponse, - type Sgg + type Sgg, } from "../model/statistics-interface"; export class StatisticsAPiService implements StatisticsService { - public async getSidoList(): Promise> { - const response = await axios.get>( + const response = await stClient.get>( `${ENDPOINT.ST_SERVICE.COMMON}/search-condition`, ); - return response.data; - }; + } public async getSggList(sidoName: string): Promise> { - const response = await axios.get>( + const response = await stClient.get>( `${ENDPOINT.ST_SERVICE.COMMON}/sgg`, - { - params: { - sidoName: sidoName, - }, - } + { params: { sidoName } }, ); - return response.data; } } diff --git a/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts index 47637923f..b0fa51d0d 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts @@ -13,15 +13,15 @@ export interface StatsInputStatusList { gugun?: string; menuCd?: string; menuNm?: string; - locgStatusNm?: string; + laeSbmsnSttsCdNm?: string; inputRate?: number; - locgCharger?: string; - locgCttpc?: string; - locgDept?: string; - locgJob?: string; - locgCnfirmNm?: string; - locgCnfirmDept?: string; - locgCnfirmJob?: string; + laePicNm?: string; + laePicTelNo?: string; + laeKbrdrDeptNm?: string; + laeKbrdrJbgdNm?: string; + laeIdfrNm?: string; + laeIdfrDeptNm?: string; + laeIdfrJbgdNm?: string; returnOpinionSm?: string; } diff --git a/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts index fb3380b6b..623d037df 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts @@ -2,14 +2,14 @@ import { z } from "zod"; export const statisticsInputSubmitInfoSchema = z.object({ // 작성자 명, 작성자 직제, 작성자 부서, 작성자 연락처, 승인자명, 승인자 직제, 승인자 부서, 승인자 연락처 (전부 필수) - locgCharger: z.string().min(1, "작성자 명을 입력하세요"), - locgJob: z.string().min(1, "작성자 직제를 입력하세요"), - locgDept: z.string().min(1, "작성자 부서를 입력하세요"), - locgCttpc: z.string().min(1, "작성자 연락처를 입력하세요"), - locgCnfirmNm: z.string().min(1, "승인자명을 입력하세요"), - locgCnfirmJob: z.string().min(1, "승인자 직제를 입력하세요"), - locgCnfirmDept: z.string().min(1, "승인자 부서를 입력하세요"), - locgCnfirmPh: z.string().min(1, "승인자 연락처를 입력하세요"), + laePicNm: z.string().min(1, "작성자 명을 입력하세요"), + laeKbrdrJbgdNm: z.string().min(1, "작성자 직제를 입력하세요"), + laeKbrdrDeptNm: z.string().min(1, "작성자 부서를 입력하세요"), + laePicTelNo: z.string().min(1, "작성자 연락처를 입력하세요"), + laeIdfrNm: z.string().min(1, "승인자명을 입력하세요"), + laeIdfrJbgdNm: z.string().min(1, "승인자 직제를 입력하세요"), + laeIdfrDeptNm: z.string().min(1, "승인자 부서를 입력하세요"), + laeIdfrTelNo: z.string().min(1, "승인자 연락처를 입력하세요"), }); export type StatisticsInputSubmitInfoFormData = z.infer< diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts index 8ed9340ea..f9ba8843d 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts @@ -2,29 +2,29 @@ import type { ApiResponse } from "./statistics-interface"; // 하수도 보급률 데이터 항목 export interface SewerageSupplyRateStatusInfo { - locgStatus: string | null; - locgDt: string | null; - locgCharger: string | null; - locgCttpc: string | null; - locgJob: string | null; + laeSbmsnSttsCd: string | null; + laeSbmsnYmd: string | null; + laePicNm: string | null; + laePicTelNo: string | null; + laeKbrdrJbgdNm: string | null; prvnStatus: string | null; dcsnStatus: string | null; wrterOpinionSm: string | null; chckerOpinionSm: string | null; returnOpinionSm: string | null; - locgCnfirmDept: string | null; - locgCnfirmJob: string | null; - locgCnfirmNm: string | null; - locgCnfirmPh: string | null; - locgDept: string | null; + laeIdfrDeptNm: string | null; + laeIdfrJbgdNm: string | null; + laeIdfrNm: string | null; + laeIdfrTelNo: string | null; + laeKbrdrDeptNm: string | null; } export interface SewerageSupplyRateCommonInfo { sewerBzmnArea: number; sewerBzmnCd: string; - sewerBzmnFrgnrPoplSm: number; - sewerBzmnPoplSm: number; - sewerBzmnTotalPopl: number; + sewerBzmnFrgnrWholPpltnCnt: number; + sewerBzmnDomplWholPpltnCnt: number; + sewerBzmnWholPpltnCnt: number; piplFcltyExtn: number; piplPlanExtn: number; swagePplPsplyRt: number; @@ -135,17 +135,17 @@ export interface SewerageSupplyRateSaveRequest { ctpvNm: string; sewerBzmnCd: string; userId: string; - sewerBzmnFrgnrPoplSm?: number; + sewerBzmnFrgnrWholPpltnCnt?: number; saveList?: SewerageSupplyRateSaveItem[]; submitList?: SewerageSupplyRateSaveItem[]; - locgCharger?: string; - locgJob?: string; - locgDept?: string; - locgCttpc?: string; - locgCnfirmNm?: string; - locgCnfirmJob?: string; - locgCnfirmDept?: string; - locgCnfirmPh?: string; + laePicNm?: string; + laeKbrdrJbgdNm?: string; + laeKbrdrDeptNm?: string; + laePicTelNo?: string; + laeIdfrNm?: string; + laeIdfrJbgdNm?: string; + laeIdfrDeptNm?: string; + laeIdfrTelNo?: string; superAdmin?: string; } diff --git a/apps/st/src/shared/api/client.ts b/apps/st/src/shared/api/client.ts index f4696eb1d..a973ae6b4 100644 --- a/apps/st/src/shared/api/client.ts +++ b/apps/st/src/shared/api/client.ts @@ -1,11 +1,18 @@ +import axios, { type AxiosInstance } from "axios"; import { Client } from "@common/network"; -import { BASE_URL } from "../config/api"; -import { SITE_ID } from "../config/env"; +import { BASE_URL, ST_SERVICE_BASE_URL } from "../config/api"; +import { AUTH_USER_ID, CLAIM_NAME, SITE_ID } from "../config/env"; import { setCookie } from "../lib"; +import Cookies from "js-cookie"; setCookie("site-id-key", SITE_ID); export const client = Client.getInstance({ baseURL: BASE_URL, siteId: SITE_ID, +}); + +export const stClient = Client.getInstance({ + baseURL: ST_SERVICE_BASE_URL, + siteId: SITE_ID, }); \ No newline at end of file diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 1b9cf74ec..6a69bcc64 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -2,6 +2,8 @@ import { PROXY_HOST } from "./env"; export const BASE_URL = `${PROXY_HOST}/server`; +export const ST_SERVICE_BASE_URL = `${PROXY_HOST}/server`; + export const ENDPOINT = { ATTACH_SERVICE: { TUS_UPLOAD: "/attach-service/api/v1/tus/upload", From 451d25666a476b0c2be7b32d8a160ac99de708b8 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 5 Mar 2026 14:10:15 +0900 Subject: [PATCH 025/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 72 +++++++------- .../model/analysis-history-interface.ts | 68 +++++++------- .../model/verification-history-column.ts | 72 -------------- .../model/verification-history-columns.tsx | 93 +++++++++++++++++++ .../model/verification-history-interface.ts | 27 ------ .../model/verification-history-mock.ts | 67 ------------- 6 files changed, 168 insertions(+), 231 deletions(-) delete mode 100644 apps/st/src/entities/statistics/model/verification-history-column.ts create mode 100644 apps/st/src/entities/statistics/model/verification-history-columns.tsx delete mode 100644 apps/st/src/entities/statistics/model/verification-history-interface.ts delete mode 100644 apps/st/src/entities/statistics/model/verification-history-mock.ts diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 2a356aa43..05db9169a 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -1,11 +1,13 @@ "use client"; import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import DataGrid from "@/components/module/DataGrid"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Button, Checkbox, + DataTable, Dialog, DialogContent, DialogHeader, @@ -27,14 +29,17 @@ import { TableRow, TableCell, } from "@common/components"; -import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column"; +import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns"; import { analysisApi } from "@/entities/statistics/api/analysis-api"; import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; import type { AnalysisRequestPayload, AnalysisResponseDto, } from "@/entities/statistics/model/analysis-interface"; -import type { StAnlsOutlierListResponse } from "@/entities/statistics/model/analysis-history-interface"; +import type { + StAnlsMstResponse, + StAnlsOutlierListResponse, +} from "@/entities/statistics/model/analysis-history-interface"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; import { CodeApi } from "@/entities/code"; @@ -70,6 +75,7 @@ function toNumber(value: string): number { } const HbStatisticsVerification = () => { + const router = useRouter(); const { setMessage: alert } = useAlertStore((state) => state); const queryClient = useQueryClient(); @@ -103,20 +109,34 @@ const HbStatisticsVerification = () => { const { data: analysisHistory } = useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], - queryFn: async () => { - const response = await analysisHistoryApi.getAnalysisHistoryList({ + queryFn: () => + analysisHistoryApi.getAnalysisHistoryList({ page: 0, size: 10, - }); - return response; + }), + refetchInterval: (query) => { + const data = query.state.data; + if (!data?.content?.length) return false; + const hasProcessing = (data.content as StAnlsMstResponse[]).some( + (item) => item.anlsSttsCd === "PROCESSING", + ); + return hasProcessing ? 3000 : false; }, }); - const historyRowData = - (analysisHistory?.content ?? []) as unknown as Record< - string, - string | number - >[]; + const historyRowData: StAnlsMstResponse[] = (analysisHistory?.content ?? + []) as StAnlsMstResponse[]; + + const table = useReactTable({ + data: historyRowData, + columns: verificationHistoryColumns, + getCoreRowModel: getCoreRowModel(), + meta: { + onOutlierCellClick: (row: StAnlsMstResponse) => { + setOutlierDialog({ open: true, anlsSn: row.anlsSn }); + }, + }, + }); const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({ queryKey: [ @@ -212,11 +232,10 @@ const HbStatisticsVerification = () => { setSelectedTargets([]); } - function handleHistoryRowClick(row: Record) { - const abnormalCount = Number(row.otlrDataNocs ?? 0); + function handleHistoryRowClick(row: StAnlsMstResponse) { const anlsSn = Number(row.anlsSn ?? 0); - if (!anlsSn || abnormalCount <= 0) return; - setOutlierDialog({ open: true, anlsSn }); + if (!anlsSn) return; + router.push(`/statistics/verification/${anlsSn}`); } function closeOutlierDialog() { @@ -417,25 +436,16 @@ const HbStatisticsVerification = () => { )}
-
+

검증 이력

- {}} - isExcelImport={false} - isExcelExport={false} - showHeaderCheckbox={false} + { - if (fieldName === "otlrDataNocs") { - const abnormal = Number(row.otlrDataNocs ?? 0); - if (abnormal > 0) return "rg-cell-verify-error"; - } - return ""; - }} + viewPagination={true} + className="[&_td]:!p-0" />
diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts index c37d4fe5c..c4aa1e0eb 100644 --- a/apps/st/src/entities/statistics/model/analysis-history-interface.ts +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -1,49 +1,49 @@ import type { PageableList } from "@/shared/model"; export interface StAnlsMstResponse { - anlsSn: number; - anlsBgngDt: string; - anlsEndDt: string | null; - anlsSttsCd: string; - anlsSttsNm: string; - nmlDataNocs: number; - exclDataNocs: number; - otlrDataNocs: number; - jobUuid: string; - regDt: string; - rgtr: string; - rgtrNm: string; + anlsSn: number; + anlsBgngDt: string; + anlsEndDt: string | null; + anlsSttsCd: string; + anlsSttsNm: string; + anlsPrgrsRt?: number; + nmlDataNocs: number; + exclDataNocs: number; + otlrDataNocs: number; + jobUuid: string; + regDt: string; + rgtr: string; + rgtrNm: string; } export interface StAnlsMstSearchParams { - anlsSttsCd?: string; - jobUuid?: string; - page?: number; - size?: number; - sort?: string; + anlsSttsCd?: string; + jobUuid?: string; + page?: number; + size?: number; + sort?: string; } export interface StAnlsOutlierDataResponse { - rsltSn: number; - vrfcTrgtSn: number; - admdstCd: string; - sido: string; - gugun: string; - adstDong: string; - addr: string; - cryrVl: number; - otlrTrsfRt: number; + rsltSn: number; + vrfcTrgtSn: number; + admdstCd: string; + sido: string; + gugun: string; + adstDong: string; + addr: string; + cryrVl: number; + otlrTrsfRt: number; } export interface StAnlsOutlierListResponse { - anlsSn: number; - vrfcTrgtSn: number; - vrfcTrgtNm: string; - nmlDataNocs: number; - exclDataNocs: number; - otlrDataNocs: number; - outlierDataList: StAnlsOutlierDataResponse[]; + anlsSn: number; + vrfcTrgtSn: number; + vrfcTrgtNm: string; + nmlDataNocs: number; + exclDataNocs: number; + otlrDataNocs: number; + outlierDataList: StAnlsOutlierDataResponse[]; } export type StAnlsMstListApiResponse = PageableList; - diff --git a/apps/st/src/entities/statistics/model/verification-history-column.ts b/apps/st/src/entities/statistics/model/verification-history-column.ts deleted file mode 100644 index e6c635a54..000000000 --- a/apps/st/src/entities/statistics/model/verification-history-column.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ValueType } from "realgrid"; -import type { ColumnInfo } from "@rsone/components"; - -/** 검증 이력 그리드 컬럼 설정 */ -export const verificationHistoryColumnInfo: ColumnInfo = { - columns: [ - { - name: "anlsBgngDt", - fieldName: "anlsBgngDt", - header: { text: "검증 실행 일시" }, - editable: false, - sortdisable: false, - }, - { - name: "anlsSttsNm", - fieldName: "anlsSttsNm", - header: { text: "진행 상태" }, - editable: false, - sortdisable: false, - }, - { - name: "anlsEndDt", - fieldName: "anlsEndDt", - header: { text: "종료 일시" }, - editable: false, - sortdisable: false, - }, - { - name: "rgtrNm", - fieldName: "rgtrNm", - header: { text: "검증자" }, - editable: false, - sortdisable: false, - }, - { - name: "nmlDataNocs", - fieldName: "nmlDataNocs", - header: { text: "정상건수" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "exclDataNocs", - fieldName: "exclDataNocs", - header: { text: "제외건수" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "otlrDataNocs", - fieldName: "otlrDataNocs", - header: { text: "이상건수" }, - editable: false, - sortdisable: false, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - ], - columnLayout: [ - "anlsBgngDt", - "anlsSttsNm", - "anlsEndDt", - "rgtrNm", - "nmlDataNocs", - "exclDataNocs", - "otlrDataNocs", - ], -}; diff --git a/apps/st/src/entities/statistics/model/verification-history-columns.tsx b/apps/st/src/entities/statistics/model/verification-history-columns.tsx new file mode 100644 index 000000000..766709217 --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-history-columns.tsx @@ -0,0 +1,93 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { Progress } from "@common/components"; +import type { StAnlsMstResponse } from "./analysis-history-interface"; + +const columnHelper = createColumnHelper(); + +export const verificationHistoryColumns = [ + columnHelper.accessor("anlsBgngDt", { + header: "검증 실행 일시", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), + columnHelper.accessor( + (row) => ({ + anlsSttsCd: row.anlsSttsCd, + anlsSttsNm: row.anlsSttsNm, + anlsPrgrsRt: row.anlsPrgrsRt, + }), + { + id: "anlsSttsNm", + header: "진행 상태", + cell: (info) => { + const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt } = info.getValue(); + if (anlsSttsCd === "PROCESSING") { + return ( +
+ + + {anlsPrgrsRt ?? 0}% + +
+ ); + } + return anlsSttsNm; + }, + }, + ), + columnHelper.accessor("anlsEndDt", { + header: "종료 일시", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), + columnHelper.accessor("rgtrNm", { + header: "검증자", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), + columnHelper.accessor("nmlDataNocs", { + header: "정상건수", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), + columnHelper.accessor("exclDataNocs", { + header: "제외건수", + cell: (info) => ( +
{info.getValue() ?? "-"}
+ ), + }), + columnHelper.accessor("otlrDataNocs", { + header: "이상건수", + cell: (info) => { + const value = info.getValue() ?? "-"; + const row = info.row.original; + const onOutlierCellClick = ( + info.table.options.meta as { + onOutlierCellClick?: (row: StAnlsMstResponse) => void; + } + )?.onOutlierCellClick; + if (value > 0 && onOutlierCellClick) { + return ( + + ); + } + return value; + }, + }), +]; diff --git a/apps/st/src/entities/statistics/model/verification-history-interface.ts b/apps/st/src/entities/statistics/model/verification-history-interface.ts deleted file mode 100644 index 869399cba..000000000 --- a/apps/st/src/entities/statistics/model/verification-history-interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** 검증 이력 한 건 (그리드 행 데이터) */ -export interface VerificationHistoryItem { - /** 검증 실행 일시 */ - executedAt: string; - /** 진행 상태 */ - progressStatus: string; - /** 종료 일시 */ - endedAt: string; - /** 검증년도 */ - verificationYear: string; - /** μ */ - mu: number; - /** Percentile */ - percentile: number; - /** A */ - a: number; - /** C */ - c: number; - /** 검증 대상 */ - target: string; - /** 정상건수 */ - normalCount: number; - /** 제외건수 */ - excludedCount: number; - /** 이상건수 */ - abnormalCount: number; -} diff --git a/apps/st/src/entities/statistics/model/verification-history-mock.ts b/apps/st/src/entities/statistics/model/verification-history-mock.ts deleted file mode 100644 index 90c152be7..000000000 --- a/apps/st/src/entities/statistics/model/verification-history-mock.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { fakerKO as faker } from "@faker-js/faker"; -import type { VerificationHistoryItem } from "./verification-history-interface"; - -const VERIFICATION_TARGETS = [ - "하수도 보급률", - "하수관로", - "하수관로 유지관리", - "하수저류시설 및 빗물펌프장", - "펌프장", - "공공하수처리시설", - "분뇨처리시설", - "개인하수처리시설", - "하수도재정", - "물 재이용", - "하수도 서비스 수준", -]; - -const PROGRESS_STATUSES = ["대기", "진행중", "완료", "실패"]; - -function formatDateTime(date: Date): string { - const y = date.getFullYear(); - const m = String(date.getMonth() + 1).padStart(2, "0"); - const d = String(date.getDate()).padStart(2, "0"); - const h = String(date.getHours()).padStart(2, "0"); - const min = String(date.getMinutes()).padStart(2, "0"); - return `${y}-${m}-${d} ${h}:${min}`; -} - -/** 검증 이력 목데이터 생성 (그리드 행 형식) */ -export function createVerificationHistoryMock( - count = 10, -): Record[] { - faker.seed(42); - return Array.from({ length: count }, () => { - const executedAt = faker.date.recent({ days: 90 }); - const durationMinutes = faker.number.int({ min: 1, max: 120 }); - const endedAt = new Date( - executedAt.getTime() + durationMinutes * 60 * 1000, - ); - const status = faker.helpers.arrayElement(PROGRESS_STATUSES); - const normalCount = faker.number.int({ min: 0, max: 500 }); - const excludedCount = faker.number.int({ min: 0, max: 50 }); - const abnormalCount = faker.number.int({ min: 0, max: 30 }); - const item: VerificationHistoryItem = { - executedAt: formatDateTime(executedAt), - progressStatus: status, - endedAt: formatDateTime(endedAt), - verificationYear: String(executedAt.getFullYear()), - mu: faker.number.float({ min: 0, max: 1, fractionDigits: 3 }), - percentile: faker.number.float({ - min: 0, - max: 100, - fractionDigits: 2, - }), - a: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }), - c: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }), - target: faker.helpers.arrayElement(VERIFICATION_TARGETS), - normalCount, - excludedCount, - abnormalCount, - }; - return { ...item } as Record; - }); -} - -/** 검증 이력 기본 목데이터 (10건) */ -export const verificationHistoryMockData = createVerificationHistoryMock(10); From 9eb0cd428b0317fe6badfa90e8ca0a887fef77d6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 5 Mar 2026 16:22:30 +0900 Subject: [PATCH 026/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20'=EA=B2=80=EC=A6=9D=EC=9D=BC=EC=8B=9C'?= =?UTF-8?q?=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 130 ++++++++++++++++-- .../model/analysis-history-interface.ts | 2 + .../components/src/ui/calendar/index.tsx | 1 - 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 05db9169a..e89bc81e8 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -1,9 +1,14 @@ "use client"; +import dayjs from "dayjs"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; import { Button, Checkbox, @@ -28,6 +33,7 @@ import { TableHeader, TableRow, TableCell, + DateRangePicker, } from "@common/components"; import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns"; import { analysisApi } from "@/entities/statistics/api/analysis-api"; @@ -38,12 +44,18 @@ import type { } from "@/entities/statistics/model/analysis-interface"; import type { StAnlsMstResponse, + StAnlsMstSearchParams, StAnlsOutlierListResponse, } from "@/entities/statistics/model/analysis-history-interface"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; import { CodeApi } from "@/entities/code"; +type DateRangeValue = { + from?: Date; + to?: Date; +}; + type VerifyType = "ALL" | "SELECT"; interface VerificationFormState { @@ -59,6 +71,12 @@ interface OutlierDialogState { anlsSn: number | null; } +function getDefaultHistoryDateRange(): DateRangeValue { + const to = dayjs().endOf("day").toDate(); + const from = dayjs().subtract(7, "day").startOf("day").toDate(); + return { from, to }; +} + function getVerificationYearOptions(): string[] { const currentYear = new Date().getFullYear(); const startYear = 2017; @@ -99,6 +117,22 @@ const HbStatisticsVerification = () => { anlsSn: null, }); + const defaultHistoryDateRange = useMemo( + () => getDefaultHistoryDateRange(), + [], + ); + const [historyDateRange, setHistoryDateRange] = useState< + DateRangeValue | undefined + >(defaultHistoryDateRange); + const [appliedHistoryDateRange, setAppliedHistoryDateRange] = useState< + DateRangeValue | undefined + >(defaultHistoryDateRange); + + const [historyPagination, setHistoryPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const { data: statsItemList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"], queryFn: async () => { @@ -108,12 +142,31 @@ const HbStatisticsVerification = () => { }); const { data: analysisHistory } = useQuery({ - queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], - queryFn: () => - analysisHistoryApi.getAnalysisHistoryList({ - page: 0, - size: 10, - }), + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, + historyPagination, + appliedHistoryDateRange, + ], + queryFn: () => { + const params: StAnlsMstSearchParams = { + page: historyPagination.pageIndex, + size: historyPagination.pageSize, + }; + + if (appliedHistoryDateRange?.from) { + params.verifyDtStart = dayjs( + appliedHistoryDateRange.from, + ).format("YYYY-MM-DD"); + } + + if (appliedHistoryDateRange?.to || appliedHistoryDateRange?.from) { + const to = + appliedHistoryDateRange.to ?? appliedHistoryDateRange.from!; + params.verifyDtEnd = dayjs(to).format("YYYY-MM-DD"); + } + + return analysisHistoryApi.getAnalysisHistoryList(params); + }, refetchInterval: (query) => { const data = query.state.data; if (!data?.content?.length) return false; @@ -131,6 +184,17 @@ const HbStatisticsVerification = () => { data: historyRowData, columns: verificationHistoryColumns, getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + manualPagination: true, + pageCount: analysisHistory?.totalPages ?? 0, + state: { + pagination: historyPagination, + }, + onPaginationChange: (updater) => { + setHistoryPagination((prev) => + typeof updater === "function" ? updater(prev) : updater, + ); + }, meta: { onOutlierCellClick: (row: StAnlsMstResponse) => { setOutlierDialog({ open: true, anlsSn: row.anlsSn }); @@ -238,6 +302,24 @@ const HbStatisticsVerification = () => { router.push(`/statistics/verification/${anlsSn}`); } + function applyHistoryDateSearch(nextRange: DateRangeValue | undefined) { + setAppliedHistoryDateRange(nextRange); + setHistoryPagination((prev) => ({ + ...prev, + pageIndex: 0, + })); + } + + function handleHistoryDateSearch() { + applyHistoryDateSearch(historyDateRange); + } + + function handleHistoryDateReset() { + const resetRange = getDefaultHistoryDateRange(); + setHistoryDateRange(resetRange); + applyHistoryDateSearch(resetRange); + } + function closeOutlierDialog() { setOutlierDialog({ open: false, anlsSn: null }); } @@ -293,7 +375,7 @@ const HbStatisticsVerification = () => {
{/* 검증 대상 */}
-

+

검증 대상

@@ -334,7 +416,7 @@ const HbStatisticsVerification = () => { {/* 검증 변수 */}
-

+

검증 변수

@@ -440,6 +522,36 @@ const HbStatisticsVerification = () => {

검증 이력

+
+ + 검증일시 + + + setHistoryDateRange((value as any) ?? undefined) + } + className="max-w-[26rem]" + /> +
+ + +
+
Date: Thu, 5 Mar 2026 17:47:59 +0900 Subject: [PATCH 027/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=8C=80=EC=83=81=20=EB=AA=A9=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 205 ++++++++++-------- .../verification-target-management-service.ts | 13 ++ ...erification-target-management-interface.ts | 8 + 3 files changed, 141 insertions(+), 85 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index e89bc81e8..f8bca9890 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -49,7 +49,9 @@ import type { } from "@/entities/statistics/model/analysis-history-interface"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; -import { CodeApi } from "@/entities/code"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; +import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; +import { CodeSelect } from "@/entities/code"; type DateRangeValue = { from?: Date; @@ -78,7 +80,7 @@ function getDefaultHistoryDateRange(): DateRangeValue { } function getVerificationYearOptions(): string[] { - const currentYear = new Date().getFullYear(); + const currentYear = dayjs().year(); const startYear = 2017; const endYear = currentYear - 1; if (endYear < startYear) return []; @@ -116,6 +118,7 @@ const HbStatisticsVerification = () => { open: false, anlsSn: null, }); + const [selectedStatsCd, setSelectedStatsCd] = useState(""); const defaultHistoryDateRange = useMemo( () => getDefaultHistoryDateRange(), @@ -133,14 +136,22 @@ const HbStatisticsVerification = () => { pageSize: 10, }); - const { data: statsItemList } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"], - queryFn: async () => { - const response = await CodeApi.getCode("STITEM"); - return response; - }, + const { data: verificationTargetCodes } = useQuery< + VerificationTargetCodeResponse[] + >({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"], + queryFn: () => + verificationTargetManagementApi.getVerificationTargetCodes(), }); + const filteredVerificationTargetCodes = useMemo(() => { + return verificationTargetCodes?.filter((item) => + selectedStatsCd + ? item.vrfcTrgtSn.startsWith(selectedStatsCd) + : true, + ); + }, [verificationTargetCodes, selectedStatsCd]); + const { data: analysisHistory } = useQuery({ queryKey: [ ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, @@ -329,26 +340,50 @@ const HbStatisticsVerification = () => {
{/* 검증 종류 선택 */}
-
- - 검증 방식 - - - setVerifyType(value as VerifyType) - } - className="flex items-center gap-4" - > - - - +
+
+ + +
+
+ + + setVerifyType(value as VerifyType) + } + className="flex items-center gap-4" + > + + + +
+
+
{verifyType === "SELECT" && - statsItemList && - statsItemList.length > 0 && ( -
- {statsItemList.map((item) => ( + filteredVerificationTargetCodes && + filteredVerificationTargetCodes.length > 0 ? ( +
+ {filteredVerificationTargetCodes.map( + (item) => (
handleTargetToggle( - item.lwpoCd, + item.vrfcTrgtSn, ) } />
- ))} -
- )} + ), + )} +
+ ) : ( +
+

+ 검증 대상이 없습니다. +

+
+ )}
{/* //검증 대상 */} @@ -482,35 +544,6 @@ const HbStatisticsVerification = () => { className="flex-1" />
- -
- - -
{/* //검증 변수 */} @@ -519,21 +552,23 @@ const HbStatisticsVerification = () => {
-

+

검증 이력

-
- - 검증일시 - - - setHistoryDateRange((value as any) ?? undefined) - } - className="max-w-[26rem]" - /> +
+
+ + 검증일시 + + + setHistoryDateRange((value as any) ?? undefined) + } + className="max-w-[26rem]" + /> +
+ +
+
+ +
+ {}} + /> +
); }; diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index 6cd283565..566f7ae48 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -24,9 +24,9 @@ export const SeweragePenetrationRateInpColumn = { { name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } }, // 행정참고자료인구 - { name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, - { name: 'poplSm', fieldName: 'poplSm', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, - { name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, + { name: 'pbadmsWholPpltnCnt', fieldName: 'pbadmsWholPpltnCnt', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, + { name: 'domplWholPpltnCnt', fieldName: 'domplWholPpltnCnt', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, + { name: 'frgnrWholPpltnCnt', fieldName: 'frgnrWholPpltnCnt', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 행정참고자료면적 { name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, @@ -207,7 +207,7 @@ export const SeweragePenetrationRateInpColumn = { name: 'groupC', hideChildHeaders: false, header: { text: '행정참고자료인구' }, - items: ['totalPopl', 'poplSm', 'frgnrPoplSm'], + items: ['pbadmsWholPpltnCnt', 'domplWholPpltnCnt', 'frgnrWholPpltnCnt'], }, 'refArea', 'wholPpltnCnt', diff --git a/apps/st/src/components/util/column/input/StatsInputStatusInpColumn.ts b/apps/st/src/components/util/column/input/StatsInputStatusInpColumn.ts index 61c450c24..7ed87ddc3 100644 --- a/apps/st/src/components/util/column/input/StatsInputStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/StatsInputStatusInpColumn.ts @@ -6,11 +6,11 @@ */ export const StatsInputStatusInpColumn = { columns: [ - { name: 'sido', fieldName: 'sido', header: { text: '시도' }, editable: false }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false }, - { name: 'menuCd', fieldName: 'menuCd', header: { text: '메뉴코드' }, visible: false, editable: false }, - { name: 'menuNm', fieldName: 'menuNm', header: { text: '구분' }, editable: false, width: 220 }, - { name: 'laeSbmsnSttsCdNm', fieldName: 'laeSbmsnSttsCdNm', header: { text: '진행상황' }, editable: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false }, + { name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false, editable: false }, + { name: 'menuTabName', fieldName: 'menuTabName', header: { text: '구분' }, editable: false, width: 220 }, + { name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false }, // { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false }, { name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false }, @@ -19,20 +19,20 @@ export const StatsInputStatusInpColumn = { { name: 'laeIdfrNm', fieldName: 'laeIdfrNm', header: { text: '승인자' }, editable: false }, { name: 'laeIdfrDeptNm', fieldName: 'laeIdfrDeptNm', header: { text: '부서' }, editable: false }, { name: 'laeIdfrJbgdNm', fieldName: 'laeIdfrJbgdNm', header: { text: '직제' }, editable: false }, - { name: 'returnOpinionSm', fieldName: 'returnOpinionSm', header: { text: '반려사유' }, editable: false }, + { name: 'rjctOpnnCn', fieldName: 'rjctOpnnCn', header: { text: '반려사유' }, editable: false }, ], columnLayout: [ - 'sido', - 'gugun', - 'menuCd', - 'menuNm', + 'ctpvNm', + 'sggNm', + 'statsCd', + 'menuTabName', { name: 'groupA', hideChildHeaders: false, header: { text: '하수도사업자' }, items: [ - 'laeSbmsnSttsCdNm', + 'locgStatusNm', // 'inputRate', 'laePicNm', 'laePicTelNo', @@ -41,7 +41,7 @@ export const StatsInputStatusInpColumn = { 'laeIdfrNm', 'laeIdfrDeptNm', 'laeIdfrJbgdNm', - 'returnOpinionSm' + 'rjctOpnnCn' ], }, ] diff --git a/apps/st/src/components/util/column/management/LogHistoryColumn.ts b/apps/st/src/components/util/column/management/LogHistoryColumn.ts new file mode 100644 index 000000000..74efa78b4 --- /dev/null +++ b/apps/st/src/components/util/column/management/LogHistoryColumn.ts @@ -0,0 +1,21 @@ +import { ValueType } from "realgrid"; + +export const LogHistoryColumn = { + columns: [ + { name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false }, + { name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false }, + { name: 'dmndFwkNm', fieldName: 'dmndFwkNm', header: { text: '요청 기능명' }, editable: false }, + { name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false }, + { name: 'regDt', fieldName: 'regDt', header: { text: '등록일' }, editable: false }, + ], + + columnLayout: [ + 'statsLogSn', + 'menuNm', + 'tblNm', + 'dmndFwkNm', + 'rgtrId', + 'regDt' + ] +}; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts index b0fa51d0d..9eb5e89af 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-status-interface.ts @@ -9,11 +9,11 @@ export interface StatsInputStatusListRequestParams { } export interface StatsInputStatusList { - sido?: string; - gugun?: string; - menuCd?: string; - menuNm?: string; - laeSbmsnSttsCdNm?: string; + ctpvNm?: string; + sggNm?: string; + statsCd?: string; + menuTabName?: string; + locgStatusNm?: string; inputRate?: number; laePicNm?: string; laePicTelNo?: string; @@ -22,7 +22,7 @@ export interface StatsInputStatusList { laeIdfrNm?: string; laeIdfrDeptNm?: string; laeIdfrJbgdNm?: string; - returnOpinionSm?: string; + rjctOpnnCn?: string; } export type StatsInputStatusListApiResponse = ApiResponse & { diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts index f9ba8843d..5c1a1f6ef 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts @@ -11,7 +11,6 @@ export interface SewerageSupplyRateStatusInfo { dcsnStatus: string | null; wrterOpinionSm: string | null; chckerOpinionSm: string | null; - returnOpinionSm: string | null; laeIdfrDeptNm: string | null; laeIdfrJbgdNm: string | null; laeIdfrNm: string | null; From 0a52b6c50739bfa9703ddc93af984e531505fe78 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 6 Mar 2026 11:42:53 +0900 Subject: [PATCH 029/113] =?UTF-8?q?[st]=20feat:=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=EA=B1=B4=EC=88=98=20=EC=83=81=EC=84=B8=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/app/styles/components.css | 129 +----------------- .../verification/HbStatisticsVerification.tsx | 112 ++++++++------- 2 files changed, 66 insertions(+), 175 deletions(-) diff --git a/apps/st/src/app/styles/components.css b/apps/st/src/app/styles/components.css index 5b9742c0f..35a341131 100644 --- a/apps/st/src/app/styles/components.css +++ b/apps/st/src/app/styles/components.css @@ -1,129 +1,4 @@ .card { - position: relative; - margin: 0.6rem; - box-sizing: border-box; - min-height: 2rem; - border-radius: 0.5rem; - padding: 0.6rem; - font-size: 1.3rem; - color: #666; -} - -.card_width10 { - width: calc(10% - 12px); -} - -.card_width20 { - width: calc(20% - 12px); -} - -.card_width25 { - width: calc(25% - 12px); -} - -.card_width30 { - width: calc(30% - 12px); -} - -.card_width33 { - width: calc(33.333% - 12px); -} - -.card_width40 { - width: calc(40% - 12px); -} - -.card_width45 { - width: calc(45% - 12px); -} - -.card_width50 { - width: calc(50% - 12px); -} - -.card_width60 { - width: calc(60% - 12px); -} - -.card_width66 { - width: calc(66.666% - 12px); -} - -.card_width70 { - width: calc(70% - 12px); -} - -.card_width75 { - width: calc(75% - 12px); -} - -.card_width80 { - width: calc(80% - 12px); -} - -.card_width90 { - width: calc(90% - 12px); -} - -.card_width100 { - width: calc(100% - 12px); -} - -.card_nopadding { - padding: 0 !important; -} - -.card_nomargin { - width: 100%; - margin: 0 !important; - padding: 0 !important; -} - -.topline { - border-top: 1px solid #ccc; -} - -.bottomline { - border-bottom: 1px solid #e6eef3; - background: #fff; -} - -.card-border { - border: 1px solid #dbe2e6; - background-color: rgba(255, 255, 255, 0.6); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); -} - -.card-header { - margin-top: -0.6rem; - margin-left: -0.6rem; - margin-right: -0.6rem; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid #e1e7eb; - padding: 1rem; -} - -.card-footer { - margin-left: -0.6rem; - margin-right: -0.6rem; - margin-top: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - border-top: 1px solid #e1e7eb; - padding-top: 1rem; - padding-bottom: 1rem; -} - -/* 블랙 테마 */ -[data-theme="black"] { - .card-border { - background-color: rgba(255, 255, 255, 0.065); - border-color: #38383a; - } - .card-header { - border-bottom-color: #3f4042; - } + margin: 0.5rem !important; + padding: 0.5rem !important; } diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index f8bca9890..3a77801d3 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -1,7 +1,7 @@ "use client"; import dayjs from "dayjs"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -152,6 +152,19 @@ const HbStatisticsVerification = () => { ); }, [verificationTargetCodes, selectedStatsCd]); + const allVerificationTargetIds = useMemo( + () => + (filteredVerificationTargetCodes ?? []).map( + (item) => item.vrfcTrgtSn, + ), + [filteredVerificationTargetCodes], + ); + + useEffect(() => { + if (!allVerificationTargetIds.length) return; + setSelectedTargets(allVerificationTargetIds); + }, [allVerificationTargetIds]); + const { data: analysisHistory } = useQuery({ queryKey: [ ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, @@ -279,7 +292,9 @@ const HbStatisticsVerification = () => { const payload: AnalysisRequestPayload = { dataset: verifyType === "ALL" - ? "ALL" + ? allVerificationTargetIds.length > 0 + ? allVerificationTargetIds.join(",") + : undefined : selectedTargets.length > 0 ? selectedTargets.join(",") : undefined, @@ -385,7 +400,7 @@ const HbStatisticsVerification = () => {
-
+
-
-

+
+

검증 이력

@@ -625,57 +640,58 @@ const HbStatisticsVerification = () => { {item.vrfcTrgtNm} + + + 시도 + + + 시군구 + + + 행정구역 + + + 시설명 + + + 입력값 + + - {Array.from({ - length: Math.ceil( - item.outlierDataList - .length / 4, + {item.outlierDataList.map( + (data) => ( + + + {data.sido ?? "-"} + + + {data.gugun ?? "-"} + + + {data.adstDong ?? + "-"} + + + {/* 시설명: DTO에 해당 필드 없음 */} + - + + + {data.cryrVl != null + ? String( + data.cryrVl, + ) + : "-"} + + ), - }).map((_, rowIdx) => ( - - {item.outlierDataList - .slice( - rowIdx * 4, - rowIdx * 4 + 4, - ) - .map((data) => ( - - {`${data.sido ?? ""}${data.adstDong ? ` ${data.adstDong}` : ""}`.trim() || - "-"} - - ))} - {/* 빈 셀 추가 */} - {item.outlierDataList.slice( - rowIdx * 4, - rowIdx * 4 + 4, - ).length < 4 && - Array.from({ - length: - 4 - - item.outlierDataList.slice( - rowIdx * 4, - rowIdx * 4 + - 4, - ).length, - }).map((_, idx) => ( - - ))} - - ))} + )} ), From 97a67bfd5507bbe13ea67089471f5e316140d70f Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 6 Mar 2026 12:04:19 +0900 Subject: [PATCH 030/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 11 ++-- apps/st/src/shared/model/api-interface.ts | 54 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 3a77801d3..183b85468 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -52,6 +52,8 @@ import { useAlertStore } from "@/shared/lib"; import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; import { CodeSelect } from "@/entities/code"; +import type { AxiosError } from "axios"; +import type { ApiErrorResponse } from "@/shared/model"; type DateRangeValue = { from?: Date; @@ -244,7 +246,7 @@ const HbStatisticsVerification = () => { const analysisMutation = useMutation< AnalysisResponseDto, - Error, + AxiosError, AnalysisRequestPayload >({ mutationFn: async (payload) => analysisApi.analyze(payload), @@ -254,8 +256,11 @@ const HbStatisticsVerification = () => { queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST], }); }, - onError: () => { - alert("검증 실행 중 오류가 발생했습니다."); + onError: (error) => { + alert( + error.response?.data?.message ?? + "검증 실행 중 오류가 발생했습니다.", + ); }, }); diff --git a/apps/st/src/shared/model/api-interface.ts b/apps/st/src/shared/model/api-interface.ts index 8218bfb0d..17707db3d 100644 --- a/apps/st/src/shared/model/api-interface.ts +++ b/apps/st/src/shared/model/api-interface.ts @@ -1,34 +1,42 @@ interface SortConfig { - empty: boolean; - sorted: boolean; - unsorted: boolean; + empty: boolean; + sorted: boolean; + unsorted: boolean; } interface PageableConfig { - sort: SortConfig; - offset: number; - pageNumber: number; - pageSize: number; - paged: boolean; - unpaged: boolean; + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; } export interface PageableList { - content: T[]; - pageable: PageableConfig; - sort: SortConfig; - totalPages: number; - totalElements: number; - number: number; - numberOfElements: number; - size: number; - first: boolean; - last: boolean; - empty: boolean; + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; } export interface PageableParams { - page?: number; - size?: number; - sort?: string; + page?: number; + size?: number; + sort?: string; +} + +export interface ApiErrorResponse { + timestamp: string; + message: string; + status: number; + code: string; + errors: unknown[]; } From 6b4dd6aba796f6efd7919f7ef0f122623b0e149a Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 6 Mar 2026 13:04:14 +0900 Subject: [PATCH 031/113] =?UTF-8?q?st:=20feat:=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 106 ++++++++++++++++-- .../statistics/management/LogHistory.tsx | 94 ++++++++-------- .../GeneralVerificationManagement.tsx | 86 ++++++++++++++ 3 files changed, 235 insertions(+), 51 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 4e193c532..36a17b225 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -1,9 +1,9 @@ -import { useRef, useEffect } from "react" +import { useRef, useEffect, useLayoutEffect } from "react" import { ValueType } from "realgrid" import RealGrid, { GridFitStyle } from "realgrid" import { RealGridReact, RGDataColumn } from "realgrid-react" -import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback } from "../type/DataGrid.type" +import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback } from "../type/DataGrid.type" import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" // RealGrid 타입 확장 @@ -62,15 +62,36 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string }; } -const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, rowData }: DataGridProps) => { +const CompactDataGrid = ({ + columnInfo, + fixedColumns, + showHeaderCheckbox = false, + rowData, + setRowData, + getGridDataRef, + cellStyleCallback, + onRowClick, +}: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); + const rowDataRef = useRef[]>(rowData); + const cellStyleCallbackRef = useRef(cellStyleCallback); + rowDataRef.current = rowData; + cellStyleCallbackRef.current = cellStyleCallback; + + useLayoutEffect(() => { + if (!cellStyleCallback || !gridRef.current?.gridView) return; + gridRef.current.gridView.refresh(); + }, [cellStyleCallback, rowData]); useEffect(() => { if (!gridRef.current?.gridView || !gridRef.current?.dataProvider) return; const gridView = gridRef.current.gridView; const dataProvider = gridRef.current.dataProvider; + if ((dataProvider as { setRows?: (r: unknown[]) => void })?.setRows && rowData) { + (dataProvider as { setRows: (r: unknown[]) => void }).setRows(rowData); + } // setDataComparer로 숫자 정렬 (setFields 미사용 → 데이터 유실·필드 중복 오류 없음) const dp = dataProvider as RealGridDataProviderComparer; @@ -231,11 +252,11 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, // footer가 단일 객체이고 valueCallback이 있는 경우 columns.forEach(col => { if (!col.footer || Array.isArray(col.footer)) return; - + try { const column = currentGridView.columnByName(col.name); if (!column || !column.footer) return; - + if (col.footer.valueCallback) { (column.footer as { valueCallback?: typeof col.footer.valueCallback }).valueCallback = col.footer.valueCallback; } @@ -243,9 +264,75 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, console.warn(`Error setting footer valueCallback for column ${col.name}:`, e); } }); + + // cellStyleCallback이 있으면 컬럼별 styleCallback 설정 + if (cellStyleCallbackRef.current) { + columns.forEach((column) => { + const baseStyleName = column.styleName; + const gridCol = currentGridView.columnByName(column.name) as { + styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string; + } | null; + if (gridCol) { + gridCol.styleCallback = (_g, dataCell) => { + const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; + const fieldName = (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? ''; + const data = rowDataRef.current; + if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? ''; + const row = data[itemIndex] as Record; + const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row }); + if (!extra) return baseStyleName ?? ''; + return baseStyleName ? `${extra} ${baseStyleName}` : extra; + }; + } + }); + } + currentGridView.refresh?.(); }, 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]); + + // 행 클릭 이벤트 + useEffect(() => { + const gridView = gridRef.current?.gridView; + if (!gridView || !onRowClick) return; + + gridView.onCellClicked = (grid, clickData) => { + if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return; + try { + const row = rowDataRef.current[clickData.itemIndex]; + onRowClick((row ?? {}) as Record); + } catch (error) { + console.error("행 데이터 가져오기 오류:", error); + } + }; + + return () => { + gridView.onCellClicked = undefined; + }; + }, [onRowClick]); return (
@@ -256,10 +343,15 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, layout={columnLayout} rows={rowData} onInitialized={() => { - const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined; + const dp = gridRef.current?.dataProvider as + | { setFields?(f: unknown[]): void; setRows?(r: Record[]): void } + | undefined; if (dp?.setFields) { dp.setFields(buildFieldsFromColumns(columns)); } + if (dp?.setRows && rowData?.length) { + dp.setRows(rowData); + } }} > {columns.map((column) => ( diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index 9cecf9ed5..0b45ce9fd 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -1,61 +1,60 @@ "use client"; import { useState } from 'react'; -import useSWR from 'swr'; +// import useSWR from 'swr'; import { Button, - Input, + // Input, Label, - Select, - SelectTrigger, - SelectContent, - SelectItem, - SelectValue + // Select, + // SelectTrigger, + // SelectContent, + // SelectItem, + // SelectValue } from "@common/components"; import DataGrid from '@/components/module/DataGrid'; +import { CodeSelect } from '@/entities/code'; import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn'; import { useAlertStore } from "@/shared/lib"; -import { - LwpoCodeApi, -} from "@/entities/code"; +// import { LwpoCodeApi } from "@/entities/code"; -const getStateCdListFetcher = async () => { - return LwpoCodeApi.getLwpoCodeList({ - sort: 'regDt,desc', - upCd: 'STITEM', - page: 0, - size: 100, - }); -} +// const getStateCdListFetcher = async () => { +// return LwpoCodeApi.getLwpoCodeList({ +// sort: 'regDt,desc', +// upCd: 'STITEM', +// page: 0, +// size: 100, +// }); +// } const LogHistory = () => { - const { setMessage: alert } = useAlertStore((state) => state); - const [lwpoCd, setLwpoCd] = useState(''); + // const { setMessage: alert } = useAlertStore((state) => state); + const [statsCd, setStatsCd] = useState(''); - const { data: lwpoCdList } = useSWR( - "lwpo-code-list", - getStateCdListFetcher, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - dedupingInterval: 2000, - shouldRetryOnError: false, - onSuccess: (data) => { - if (!data.content || data.content.length == 0) { - alert(`시도 목록을 불러오는데 실패했습니다. - 새로고침 후 다시 시도해주세요.`); - } - }, - onError: () => { - alert(`시도 목록을 불러오는데 실패했습니다. - 새로고침 후 다시 시도해주세요.`); - }, - } - ); + // const { data: lwpoCdList } = useSWR( + // "lwpo-code-list", + // getStateCdListFetcher, + // { + // revalidateOnFocus: false, + // revalidateOnReconnect: false, + // revalidateIfStale: false, + // dedupingInterval: 2000, + // shouldRetryOnError: false, + // onSuccess: (data) => { + // if (!data.content || data.content.length == 0) { + // alert(`시도 목록을 불러오는데 실패했습니다. + // 새로고침 후 다시 시도해주세요.`); + // } + // }, + // onError: () => { + // alert(`시도 목록을 불러오는데 실패했습니다. + // 새로고침 후 다시 시도해주세요.`); + // }, + // } + // ); return (
@@ -64,9 +63,9 @@ const LogHistory = () => {
- @@ -78,7 +77,14 @@ const LogHistory = () => { ))} - + */} +
+ +
+
+ +
+
+
+ + + No + 검증항목 + 등록자 + 등록일 + 사용여부 + + + + + 1 + + + 인구수검증 + + + 관리자 + + + 2026-02-09 + + + Y + + + +
+
+
+ +
+
+
+ ); +}; + +export default GeneralVerificationManagement; \ No newline at end of file From 64e7a1f43f5828e223374788f35b1030f7dc53b5 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 6 Mar 2026 16:41:09 +0900 Subject: [PATCH 032/113] =?UTF-8?q?import=20=EB=AA=A8=EB=93=88=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20ESLint=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/eslint.config.mjs | 4 +- apps/st/package.json | 1 + eslint.config.mjs | 38 ++++++++++ package.json | 3 + pnpm-lock.yaml | 150 +++++++++++++++++++++++++++++++++++++- 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 eslint.config.mjs diff --git a/apps/st/eslint.config.mjs b/apps/st/eslint.config.mjs index 18107f0ca..55ef02fb5 100644 --- a/apps/st/eslint.config.mjs +++ b/apps/st/eslint.config.mjs @@ -1,6 +1,7 @@ +import { FlatCompat } from "@eslint/eslintrc"; import { dirname } from "path"; import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import baseEsLintConfig from "../../eslint.config.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,6 +12,7 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + ...baseEsLintConfig, { rules: { "react-hooks/exhaustive-deps": "off", diff --git a/apps/st/package.json b/apps/st/package.json index cd951af0d..c874cf19f 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -62,6 +62,7 @@ "@types/webpack": "^5.28.5", "eslint": "^9", "eslint-config-next": "15.1.7", + "eslint-plugin-react-hooks": "^5.0.0", "postcss": "^8", "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.6.11", diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..0b4394396 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,38 @@ +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import tsParser from "@typescript-eslint/parser"; + +export default [ + { + files: ["**/*.{js,jsx,ts,tsx}"], + languageOptions: { + parser: tsParser, + }, + plugins: { + "simple-import-sort": simpleImportSort, + }, + rules: { + "simple-import-sort/imports": [ + "warn", + { + groups: [ + ["^react", "^next"], + ["^@?\\w"], + [ + "^@/shared", + "^@/entities", + "^@/features", + "^@/widgets", + "^@/app", + "^@common", + ], + ["^\\u0000"], + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + ["^\\./(?=.*/)", "^\\.(?!/?$)", "^\\./?$"], + ["^.+\\.?(css|scss)$"], + ], + }, + ], + "simple-import-sort/exports": "warn", + }, + }, +]; diff --git a/package.json b/package.json index f48207433..5474fef3c 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,12 @@ "zustand": "^5.0.3" }, "devDependencies": { + "@eslint/eslintrc": "^3", "@types/node": "^22.13.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "@typescript-eslint/parser": "^8.56.1", + "eslint-plugin-simple-import-sort": "^12.1.1", "husky": "^9.1.7", "lint-staged": "^16.2.7", "turbo": "2.4.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a7066496..b1bb15fae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: specifier: ^5.0.3 version: 5.0.9(@types/react@19.2.7)(immer@10.2.0)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.3 '@types/node': specifier: ^22.13.1 version: 22.19.2 @@ -41,6 +44,12 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) + '@typescript-eslint/parser': + specifier: ^8.56.1 + version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@9.39.2(jiti@2.6.1)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -51,7 +60,7 @@ importers: specifier: 2.4.2 version: 2.4.2 - apps/(template): + apps/_template: dependencies: '@common/assets': specifier: workspace:* @@ -1182,6 +1191,9 @@ importers: eslint-config-next: specifier: 15.1.7 version: 15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.2.0(eslint@9.39.2(jiti@2.6.1)) postcss: specifier: ^8 version: 8.5.6 @@ -3883,22 +3895,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.49.0': resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.49.0': resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.49.0': resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3910,12 +3945,22 @@ packages: resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.49.0': resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.49.0': resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3927,6 +3972,10 @@ packages: resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -4289,6 +4338,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-arraybuffer@1.0.2: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} @@ -4311,6 +4364,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4948,6 +5005,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + eslint-plugin-simple-import-sort@12.1.1: + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + eslint-plugin-storybook@0.11.6: resolution: {integrity: sha512-3WodYD6Bs9ACqnB+TP2TuLh774c/nacAjxSKOP9bHJ2c8rf+nrhocxjjeAWNmO9IPkFIzTKlcl0vNXI2yYpVOw==} engines: {node: '>= 18'} @@ -4970,6 +5032,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5828,6 +5894,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -6947,6 +7017,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -9356,6 +9432,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.49.0(typescript@5.6.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.6.3) @@ -9374,11 +9462,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.6.3)': dependencies: typescript: 5.6.3 @@ -9387,6 +9489,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 @@ -9425,6 +9531,8 @@ snapshots: '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.6.3)': dependencies: '@typescript-eslint/project-service': 8.49.0(typescript@5.6.3) @@ -9455,6 +9563,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) @@ -9493,6 +9616,11 @@ snapshots: '@typescript-eslint/types': 8.49.0 eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -9902,6 +10030,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-arraybuffer@1.0.2: {} baseline-browser-mapping@2.9.7: {} @@ -9921,6 +10051,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -10844,6 +10978,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-plugin-storybook@0.11.6(eslint@9.39.2(jiti@2.6.1))(typescript@5.6.3): dependencies: '@storybook/csf': 0.1.13 @@ -10868,6 +11006,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.2(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) @@ -11716,6 +11856,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -12892,6 +13036,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} From f3ba3d9b319346da3100f09eeeb40e8866c032e0 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 9 Mar 2026 11:57:23 +0900 Subject: [PATCH 033/113] =?UTF-8?q?st:=20fix:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/shared/api/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/st/src/shared/api/client.ts b/apps/st/src/shared/api/client.ts index a973ae6b4..61368d875 100644 --- a/apps/st/src/shared/api/client.ts +++ b/apps/st/src/shared/api/client.ts @@ -1,9 +1,7 @@ -import axios, { type AxiosInstance } from "axios"; import { Client } from "@common/network"; import { BASE_URL, ST_SERVICE_BASE_URL } from "../config/api"; -import { AUTH_USER_ID, CLAIM_NAME, SITE_ID } from "../config/env"; +import { SITE_ID } from "../config/env"; import { setCookie } from "../lib"; -import Cookies from "js-cookie"; setCookie("site-id-key", SITE_ID); From ff2b79aaa619033a00529c3cc9ef289f0dba9618 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 9 Mar 2026 17:36:25 +0900 Subject: [PATCH 034/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/package.json | 1 + apps/st/src/app/styles/components.css | 4 - .../verification/HbStatisticsVerification.tsx | 577 ++++++++++-------- .../detail/HbVerificationDetail.tsx | 87 +++ .../verification/detail/Indicators.tsx | 86 +++ .../verification/detail/RegionalGrid.tsx | 269 ++++++++ .../verification/detail/ScatterChart.tsx | 253 ++++++++ .../verification/detail/VerificationList.tsx | 141 +++++ .../api/statistics-analysis-detail-api.ts | 36 ++ .../api/statistics-analysis-result-api.ts | 37 ++ .../statistics-analysis-detail-interface.ts | 28 + .../statistics-analysis-result-interface.ts | 46 ++ apps/st/src/shared/config/api.ts | 2 + .../src/ui/date-range-picker/index.tsx | 5 +- pnpm-lock.yaml | 75 ++- 15 files changed, 1347 insertions(+), 300 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx create mode 100644 apps/st/src/components/statistics/verification/detail/Indicators.tsx create mode 100644 apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx create mode 100644 apps/st/src/components/statistics/verification/detail/ScatterChart.tsx create mode 100644 apps/st/src/components/statistics/verification/detail/VerificationList.tsx create mode 100644 apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts create mode 100644 apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts diff --git a/apps/st/package.json b/apps/st/package.json index c874cf19f..9880cab30 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -34,6 +34,7 @@ "@types/xlsx": "^0.0.36", "axios": "^1.8.1", "chart.js": "^4.4.8", + "chartjs-plugin-zoom": "^2.2.0", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "jszip": "^3.10.1", diff --git a/apps/st/src/app/styles/components.css b/apps/st/src/app/styles/components.css index 35a341131..e69de29bb 100644 --- a/apps/st/src/app/styles/components.css +++ b/apps/st/src/app/styles/components.css @@ -1,4 +0,0 @@ -.card { - margin: 0.5rem !important; - padding: 0.5rem !important; -} diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 183b85468..0f44b8e4a 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -1,18 +1,39 @@ "use client"; -import dayjs from "dayjs"; import { useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { getCoreRowModel, getPaginationRowModel, useReactTable, } from "@tanstack/react-table"; +import type { AxiosError } from "axios"; +import dayjs from "dayjs"; + +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib"; +import type { ApiErrorResponse } from "@/shared/model"; +import { CodeSelect } from "@/entities/code"; +import { analysisApi } from "@/entities/statistics/api/analysis-api"; +import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; +import type { + StAnlsMstResponse, + StAnlsMstSearchParams, + StAnlsOutlierListResponse, +} from "@/entities/statistics/model/analysis-history-interface"; +import type { + AnalysisRequestPayload, + AnalysisResponseDto, +} from "@/entities/statistics/model/analysis-interface"; +import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns"; +import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; import { Button, Checkbox, DataTable, + DateRangePicker, Dialog, DialogContent, DialogHeader, @@ -29,34 +50,16 @@ import { SelectValue, Table, TableBody, + TableCell, TableHead, TableHeader, TableRow, - TableCell, - DateRangePicker, } from "@common/components"; -import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns"; -import { analysisApi } from "@/entities/statistics/api/analysis-api"; -import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; -import type { - AnalysisRequestPayload, - AnalysisResponseDto, -} from "@/entities/statistics/model/analysis-interface"; -import type { - StAnlsMstResponse, - StAnlsMstSearchParams, - StAnlsOutlierListResponse, -} from "@/entities/statistics/model/analysis-history-interface"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib"; -import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; -import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; -import { CodeSelect } from "@/entities/code"; -import type { AxiosError } from "axios"; -import type { ApiErrorResponse } from "@/shared/model"; + +import HbVerificationDetail from "./detail/HbVerificationDetail"; type DateRangeValue = { - from?: Date; + from: Date; to?: Date; }; @@ -97,7 +100,6 @@ function toNumber(value: string): number { } const HbStatisticsVerification = () => { - const router = useRouter(); const { setMessage: alert } = useAlertStore((state) => state); const queryClient = useQueryClient(); @@ -121,6 +123,7 @@ const HbStatisticsVerification = () => { anlsSn: null, }); const [selectedStatsCd, setSelectedStatsCd] = useState(""); + const [selectedAnlsSn, setSelectedAnlsSn] = useState(null); const defaultHistoryDateRange = useMemo( () => getDefaultHistoryDateRange(), @@ -330,7 +333,7 @@ const HbStatisticsVerification = () => { function handleHistoryRowClick(row: StAnlsMstResponse) { const anlsSn = Number(row.anlsSn ?? 0); if (!anlsSn) return; - router.push(`/statistics/verification/${anlsSn}`); + setSelectedAnlsSn(anlsSn); } function applyHistoryDateSearch(nextRange: DateRangeValue | undefined) { @@ -357,264 +360,298 @@ const HbStatisticsVerification = () => { return ( <> -
- {/* 검증 종류 선택 */} -
-
-
- - -
-
- - - setVerifyType(value as VerifyType) - } - className="flex items-center gap-4" - > - - - -
-
-
- - -
-
- {/* //검증 종류 선택 */} - - {verifyType === "SELECT" && ( -
- {/* 검증 대상 */} -
-

- 검증 대상 -

-
- - + {selectedAnlsSn ? ( + setSelectedAnlsSn(null)} + /> + ) : ( + <> +
+ {/* 검증 종류 선택 */} +
+
+
+ + +
+
+ + + setVerifyType(value as VerifyType) + } + className="flex items-center gap-4" + > + + + +
+
+
+
-
- {verifyType === "SELECT" && - filteredVerificationTargetCodes && - filteredVerificationTargetCodes.length > 0 ? ( -
- {filteredVerificationTargetCodes.map( - (item) => ( -
- - handleTargetToggle( - item.vrfcTrgtSn, - ) - } - /> - -
- ), +
+ {/* //검증 종류 선택 */} + + {verifyType === "SELECT" && ( +
+ {/* 검증 대상 */} +
+

+ 검증 대상 +

+
+ + + +
+
+ {verifyType === "SELECT" && + filteredVerificationTargetCodes && + filteredVerificationTargetCodes.length > + 0 ? ( +
+ {filteredVerificationTargetCodes.map( + (item) => ( +
+ + handleTargetToggle( + item.vrfcTrgtSn, + ) + } + /> + +
+ ), + )} +
+ ) : ( +
+

+ 검증 대상이 없습니다. +

+
)}
- ) : ( -
-

- 검증 대상이 없습니다. -

+
+ {/* //검증 대상 */} + + {/* 검증 변수 */} +
+

+ 검증 변수 +

+ +
+
+ + + handleFormChange( + "mu", + event.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleFormChange( + "percentile", + event.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleFormChange( + "a", + event.target.value, + ) + } + className="flex-1" + /> +
+ +
+ + + handleFormChange( + "c", + event.target.value, + ) + } + className="flex-1" + /> +
- )} +
+ {/* //검증 변수 */}
-
- {/* //검증 대상 */} - - {/* 검증 변수 */} -
-

- 검증 변수 -

- -
-
- - - handleFormChange( - "mu", - event.target.value, - ) - } - className="flex-1" - /> -
- -
- - - handleFormChange( - "percentile", - event.target.value, - ) - } - className="flex-1" - /> -
- -
- - - handleFormChange( - "a", - event.target.value, - ) - } - className="flex-1" - /> -
- -
- - - handleFormChange( - "c", - event.target.value, - ) - } - className="flex-1" - /> -
-
-
- {/* //검증 변수 */} + )}
- )} -
-
-

- 검증 이력 -

-
-
- - 검증일시 - - - setHistoryDateRange((value as any) ?? undefined) - } - className="max-w-[26rem]" +
+

+ 검증 이력 +

+
+
+ + 검증일시 + + + setHistoryDateRange( + value?.from + ? (value as DateRangeValue) + : undefined, + ) + } + className="max-w-[26rem]" + /> +
+
+ + +
+
+
-
- - -
-
- -
+ + )} diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx new file mode 100644 index 000000000..11eb6c8fd --- /dev/null +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -0,0 +1,87 @@ +import React, { useState } from "react"; + +import { Button } from "@common/components"; + +import AreaDIndicators from "./Indicators"; +import AreaBRegionalGrid from "./RegionalGrid"; +import AreaCScatterChart from "./ScatterChart"; +import AreaAVerificationList from "./VerificationList"; + +interface HbVerificationDetailProps { + anlsSn: number; + onBack: () => void; +} + +const HbVerificationDetail: React.FC = ({ + anlsSn, + onBack, +}) => { + const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState( + null, + ); + const [selectedSido, setSelectedSido] = useState(null); + + return ( +
+
+ +

+ 검증 상세 +

+
+ +
+
+ +
+ +
+

+ 검증 결과 상세 +

+
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ ); +}; + +export default HbVerificationDetail; diff --git a/apps/st/src/components/statistics/verification/detail/Indicators.tsx b/apps/st/src/components/statistics/verification/detail/Indicators.tsx new file mode 100644 index 000000000..eaabf7849 --- /dev/null +++ b/apps/st/src/components/statistics/verification/detail/Indicators.tsx @@ -0,0 +1,86 @@ +import React from "react"; + +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; +import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; + +interface IndicatorsProps { + anlsSn: number; + vrfcTrgtSn: string | null; +} + +const Indicators: React.FC = ({ anlsSn, vrfcTrgtSn }) => { + const { data: chartData, isLoading } = useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "chart", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); + + const dtl = chartData?.dtl; + + if (!vrfcTrgtSn) { + return ( +
+ 항목을 선택해주세요 +
+ ); + } + + if (isLoading) { + return ( +
+ 지표 데이터를 불러오는 중... +
+ ); + } + + const IndicatorItem = ({ + label, + value, + }: { + label: string; + value: number | null | undefined; + }) => ( +
+

+ {label} +

+
+ {value != null ? Number(value).toFixed(4) : "-"} +
+
+ ); + + return ( +
+ + + + + + + + +
+ ); +}; + +export default Indicators; diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx new file mode 100644 index 000000000..940d328d9 --- /dev/null +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -0,0 +1,269 @@ +import React, { useMemo, useState } from "react"; + +import { useQuery } from "@tanstack/react-query"; +import { + type ColumnDef, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "@/entities/code/api/code-service"; +import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; +import { + Button, + DataTable, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +interface RegionalGridProps { + anlsSn: number; + vrfcTrgtSn: string | null; + selectedSido: string | null; + onSidoSelect: (sido: string | null) => void; +} + +const RegionalGrid: React.FC = ({ + anlsSn, + vrfcTrgtSn, + selectedSido, + onSidoSelect, +}) => { + const [selectedGugun, setSelectedGugun] = useState("ALL"); + const [selectedStatus, setSelectedStatus] = useState("ALL"); + + const { data: regionCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], + queryFn: () => CodeApi.getCode("LAESECD"), + }); + + const sidoOptions = useMemo(() => { + if (!regionCodes) return []; + return regionCodes.filter( + (code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4, + ); + }, [regionCodes]); + + const gugunOptions = useMemo(() => { + if (!regionCodes || !selectedSido || selectedSido === "ALL") return []; + + const sidoPrefix = selectedSido; + return regionCodes.filter( + (code) => + code.lwpoCd.startsWith(sidoPrefix) && code.lwpoCd.length === 7, + ); + }, [regionCodes, selectedSido]); + + const { + data: resultData, + isLoading, + refetch, + } = useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "list", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => + stAnlsRsltApi.getResultList({ + anlsSn, + vrfcTrgtSn: Number(vrfcTrgtSn), + page: 0, + size: 1000, + }), + enabled: false, + }); + + const rowData = useMemo(() => { + let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; + + if (selectedSido && selectedSido !== "ALL") { + const sidoName = sidoOptions.find( + (opt) => opt.lwpoCd === selectedSido, + )?.lwpoCdNm; + if (sidoName) { + data = data.filter((item) => item.sido === sidoName); + } + } + + if (selectedGugun && selectedGugun !== "ALL") { + const gugunName = gugunOptions.find( + (opt) => opt.lwpoCd === selectedGugun, + )?.lwpoCdNm; + if (gugunName) { + data = data.filter((item) => item.gugun === gugunName); + } + } + + if (selectedStatus && selectedStatus !== "ALL") { + const statusTarget = selectedStatus === "NORMAL" ? "y" : "n"; + data = data.filter( + (item) => item.otlrYn.toLowerCase() === statusTarget, + ); + } + + return data; + }, [ + resultData, + selectedSido, + selectedGugun, + selectedStatus, + sidoOptions, + gugunOptions, + ]); + + const columns: ColumnDef[] = useMemo( + () => [ + { header: "시도", accessorKey: "sido", size: 80 }, + { header: "구군", accessorKey: "gugun", size: 80 }, + { header: "행정구역", accessorKey: "admdstNm", size: 100 }, + { header: "이전년도", accessorKey: "prvyrVl", size: 80 }, + { header: "당해년도", accessorKey: "cryrVl", size: 80 }, + { + header: "R", + accessorKey: "chgRt", + size: 60, + cell: (info) => Number(info.getValue() ?? 0).toFixed(2), + }, + { + header: "S", + accessorKey: "trsfRt", + size: 60, + cell: (info) => Number(info.getValue() ?? 0).toFixed(2), + }, + { + header: "E", + accessorKey: "otlrTrsfRt", + size: 60, + cell: (info) => Number(info.getValue() ?? 0).toFixed(2), + }, + { + header: "상태", + accessorKey: "otlrYn", + size: 60, + cell: (info) => { + const val = info.getValue(); + if (val === "y") + return ( + + 정상 + + ); + if (val === "n") + return ( + + 이상치 + + ); + return "-"; + }, + }, + ], + [], + ); + + const table = useReactTable({ + data: rowData, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + initialState: { + pagination: { pageSize: 10 }, + }, + }); + + const handleSidoChange = (val: string) => { + onSidoSelect(val === "ALL" ? null : val); + setSelectedGugun("ALL"); + }; + + return ( +
+
+
+ + + +
+ +
+ + +
+
+ +
+ + {isLoading && ( +
+ 데이터를 불러오는 중... +
+ )} + {!isLoading && (!vrfcTrgtSn || rowData.length === 0) && ( +
+ {vrfcTrgtSn + ? "표시할 항목이 없습니다." + : "항목을 선택해주세요"} +
+ )} +
+
+ ); +}; + +export default RegionalGrid; diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx new file mode 100644 index 000000000..97eef64c3 --- /dev/null +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -0,0 +1,253 @@ +import React, { useMemo } from "react"; +import { Scatter } from "react-chartjs-2"; + +import { useQuery } from "@tanstack/react-query"; +import { + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + ScatterController, + Tooltip, +} from "chart.js"; +import zoomPlugin from "chartjs-plugin-zoom"; + +import { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "@/entities/code/api/code-service"; +import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; + +ChartJS.register( + LinearScale, + PointElement, + LineElement, + Tooltip, + Legend, + ScatterController, + zoomPlugin, +); + +interface ScatterChartProps { + anlsSn: number; + vrfcTrgtSn: string | null; + selectedSido: string | null; +} + +const ScatterChart: React.FC = ({ + anlsSn, + vrfcTrgtSn, + selectedSido, +}) => { + const { data: regionCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], + queryFn: () => CodeApi.getCode("LAESECD"), + }); + + const selectedSidoName = useMemo(() => { + if (!selectedSido || !regionCodes) return null; + return ( + regionCodes.find((c) => c.lwpoCd === selectedSido)?.lwpoCdNm || null + ); + }, [regionCodes, selectedSido]); + + const { data: resultData, isLoading: isLoadingGrid } = useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "list", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => + stAnlsRsltApi.getResultList({ + anlsSn, + vrfcTrgtSn: Number(vrfcTrgtSn), + page: 0, + size: 1000, + }), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); + + const { data: chartData, isLoading: isLoadingChart } = useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "chart", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); + + const isLoading = isLoadingGrid || isLoadingChart; + + const chartConfig = useMemo(() => { + if (!resultData || !chartData) return null; + + const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[]; + const boundaries = chartData.list ?? []; + + const sortedBoundaries = [...boundaries].sort( + (a, b) => a.xPrev - b.xPrev, + ); + const upperLineData = sortedBoundaries.map((b) => ({ + x: b.xPrev, + y: b.yUpper, + })); + const lowerLineData = sortedBoundaries.map((b) => ({ + x: b.xPrev, + y: b.yLower, + })); + + const normalPoints: { + x: number; + y: number; + item: StAnlsRsltResponse; + }[] = []; + const outlierPoints: { + x: number; + y: number; + item: StAnlsRsltResponse; + }[] = []; + + rawPoints.forEach((item) => { + if (selectedSidoName && item.sido !== selectedSidoName) { + return; + } + + const pt = { + x: Number(item.prvyrVl ?? 0), + y: Number(item.cryrVl ?? 0), + item, + }; + + if (item.otlrYn.toLowerCase() === "y") { + normalPoints.push(pt); + } else { + outlierPoints.push(pt); + } + }); + + const data = { + datasets: [ + { + label: "상한선", + data: upperLineData, + type: "line" as const, + borderColor: "rgba(255, 99, 132, 0.8)", + borderWidth: 2, + fill: false, + pointRadius: 0, + tension: 0.1, + }, + { + label: "하한선", + data: lowerLineData, + type: "line" as const, + borderColor: "rgba(54, 162, 235, 0.8)", + borderWidth: 2, + fill: false, + pointRadius: 0, + tension: 0.1, + }, + { + label: "이상치", + data: outlierPoints, + backgroundColor: "rgba(255, 99, 132, 0.7)", + pointRadius: 5, + pointHoverRadius: 8, + }, + { + label: "정상", + data: normalPoints, + backgroundColor: "rgba(75, 192, 192, 0.7)", + pointRadius: 4, + pointHoverRadius: 7, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + zoom: { + pan: { + enabled: true, + mode: "xy" as const, + }, + zoom: { + wheel: { + enabled: true, + }, + pinch: { + enabled: true, + }, + mode: "xy" as const, + }, + }, + tooltip: { + callbacks: { + label: (context: any) => { + if (context.dataset.type === "line") { + return `${context.dataset.label}: (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`; + } + const pt = context.raw as { + x: number; + y: number; + item: StAnlsRsltResponse; + }; + return `${pt.item.sido} ${pt.item.gugun}: 이전(${pt.x}), 당해(${pt.y})`; + }, + }, + }, + }, + scales: { + x: { + type: "linear" as const, + position: "bottom" as const, + title: { + display: true, + text: "이전년도 값", + }, + }, + y: { + type: "linear" as const, + title: { + display: true, + text: "당해년도 값", + }, + }, + }, + }; + + return { data, options }; + }, [resultData, chartData, selectedSidoName]); + + return ( +
+ {!vrfcTrgtSn ? ( +
+ 항목을 선택해주세요 +
+ ) : isLoading ? ( +
+ 차트를 불러오는 중... +
+ ) : chartConfig ? ( +
+ +
+ ) : ( +
+ 표시할 데이터가 없습니다. +
+ )} +
+ ); +}; + +export default ScatterChart; diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx new file mode 100644 index 000000000..62463b036 --- /dev/null +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useMemo } from "react"; + +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; + +import { ENDPOINT } from "@/shared/config"; +import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; +import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface"; +import { DataTable } from "@common/components"; + +interface VerificationListProps { + anlsSn: number; + selectedVrfcTrgtSn: string | null; + onSelect: (vrfcTrgtSn: string) => void; +} + +const VerificationList: React.FC = ({ + anlsSn, + selectedVrfcTrgtSn, + onSelect, +}) => { + const { data: verificationTargetCodes } = useQuery({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"], + queryFn: () => + verificationTargetManagementApi.getVerificationTargetCodes(), + }); + + const targetCodeMap = useMemo(() => { + const map = new Map(); + if (verificationTargetCodes) { + verificationTargetCodes.forEach((item) => { + map.set(item.vrfcTrgtSn, item.vrfcTrgtNm); + }); + } + return map; + }, [verificationTargetCodes]); + + const { data: detailData, isLoading } = useQuery({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn], + queryFn: () => + stAnlsDtlApi.getDetailList({ anlsSn, page: 0, size: 100 }), + enabled: !!anlsSn, + }); + + const rowData = useMemo(() => { + return (detailData?.content ?? []) as StAnlsDtlResponse[]; + }, [detailData]); + + const columns = useMemo( + () => [ + { + header: "검증 항목", + accessorFn: (row: StAnlsDtlResponse) => + targetCodeMap.get(String(row.vrfcTrgtSn)) || + String(row.vrfcTrgtSn), + id: "vrfcTrgtNm", + size: 150, + }, + { + header: "μ", + accessorKey: "ipcrCntVl", + size: 60, + }, + { + header: "Percentile", + accessorKey: "pctlCrtrVl", + size: 60, + }, + { + header: "A", + accessorKey: "scopAjmtCntVl", + size: 60, + }, + { + header: "C", + accessorKey: "thrsCntVl", + size: 60, + }, + { + header: "정상", + accessorKey: "nmlDataNocs", + size: 70, + }, + { + header: "제외", + accessorKey: "exclDataNocs", + size: 70, + }, + { + header: "이상", + accessorKey: "otlrDataNocs", + size: 70, + }, + ], + [targetCodeMap], + ); + + const table = useReactTable({ + data: rowData, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + const handleRowClick = (row: StAnlsDtlResponse) => { + onSelect(String(row.vrfcTrgtSn)); + }; + + useEffect(() => { + if (!selectedVrfcTrgtSn && rowData.length > 0) { + const firstTargetSn = rowData[0]?.vrfcTrgtSn; + if (firstTargetSn) onSelect(String(firstTargetSn)); + } + }, [selectedVrfcTrgtSn, rowData, onSelect]); + + return ( +
+ {isLoading && ( +
+ 데이터를 불러오는 중... +
+ )} + {!isLoading && rowData.length === 0 && ( +
+ 표시할 항목이 없습니다. +
+ )} + {!isLoading && rowData.length > 0 && ( + + )} +
+ ); +}; + +export default VerificationList; diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts new file mode 100644 index 000000000..4e7a6e476 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-analysis-detail-api.ts @@ -0,0 +1,36 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + StAnlsDtlListApiResponse, + StAnlsDtlResponse, + StAnlsDtlSearchParams, +} from "../model/statistics-analysis-detail-interface"; + +export class StAnlsDtlApiService { + public async getDetailList( + params: StAnlsDtlSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}`, + { + params, + }, + ); + + return response.data; + } + + public async getDetail( + anlsSn: number, + vrfcTrgtSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}/${anlsSn}/${vrfcTrgtSn}`, + ); + + return response.data; + } +} + +export const stAnlsDtlApi = new StAnlsDtlApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts new file mode 100644 index 000000000..604e4144a --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts @@ -0,0 +1,37 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ChartRetDto, + StAnlsRsltListApiResponse, + StAnlsRsltSearchParams, +} from "../model/statistics-analysis-result-interface"; + +export class StAnlsRsltApiService { + public async getResultList( + params: StAnlsRsltSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}`, + { + params, + }, + ); + + return response.data; + } + + public async getChartData( + anlsSn: number, + vrfcTrgtSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/chartList/${anlsSn}/${vrfcTrgtSn}`, + ); + + return response.data; + } +} + +export const stAnlsRsltApi = new StAnlsRsltApiService(); + diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts new file mode 100644 index 000000000..3f6eb9003 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts @@ -0,0 +1,28 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface StAnlsDtlResponse { + anlsSn: number; // 분석 일련번호 + vrfcTrgtSn: number; // 검증 대상 일련번호 + ipcrCntVl: number | null; // 중요도 개수 값 (mu) + pctlCrtrVl: number | null; // 백분위 기준 값 (percentile) + scopAjmtCntVl: number | null; // 범위 조정 개수 값 (A) + thrsCntVl: number | null; // 임계 개수 값 (C) + rtCnterVl: number | null; // 비율 중앙값 + lastTrsfScrCnterVl: number | null; // 최종 변환 점수 중앙값 + lastTrsfScrLlmtVl: number | null; // 최종 변환 점수 하한값 + lastTrsfScrUplmtVl: number | null; // 최종 변환 점수 상한값 + uplmtVl: number | null; // 상한값 + llmtVl: number | null; // 하한값 + otlrUplmtLine: number | null; // 이상치 상한선 + otlrLlmtLine: number | null; // 이상치 하한선 + nmlDataNocs: number; // 정상 데이터 건수 + exclDataNocs: number; // 제외 데이터 건수 + otlrDataNocs: number; // 이상치 데이터 건수 +} + +export interface StAnlsDtlSearchParams extends PageableParams { + anlsSn?: number; // 분석 일련번호 + vrfcTrgtSn?: number; // 검증 대상 일련번호 +} + +export type StAnlsDtlListApiResponse = PageableList; diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts new file mode 100644 index 000000000..38c44b7a9 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -0,0 +1,46 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +import type { StAnlsDtlResponse } from "./statistics-analysis-detail-interface"; + +export interface StAnlsRsltResponse { + rsltSn: number; // 결과 일련번호 + anlsSn: number; // 분석 일련번호 + vrfcTrgtSn: number; // 검증 대상 일련번호 + admdstCd: string; // 행정구역 코드 + admdstNm: string; // 행정구역 명칭 + sido: string; // 시도 + gugun: string; // 시군구 + adstDong: string; // 행정동 + drngBzmnCd: string; // 하수도 사업자 코드 + pipeSeCd: string; // 파이프 구분 코드 + fcltNm: string; // 시설명 + addr: string; // 주소 + prvyrVl: number | null; // 전년도 값 + cryrVl: number | null; // 당해년도 값 + chgRt: number | null; // 변경율 (r_i) + trsfRt: number | null; // 변환 비율 (s_i) + otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i) + otlrYn: string; // 이상치 여부 (y/n) +} + +export interface StAnlsRsltSearchParams extends PageableParams { + anlsSn?: number; // 분석 일련번호 + vrfcTrgtSn?: number; // 검증 대상 일련번호 + admdstCd?: string; // 행정구역 코드 + admdstNm?: string; // 행정구역 명칭 + fcltNm?: string; // 시설명 + otlrYn?: string; // 이상치 여부 (y/n) +} + +export type StAnlsRsltListApiResponse = PageableList; + +export interface ChartBoundaryPointDto { + xPrev: number; // 전년도 값 (x축) + yLower: number; // 이상치 하한 당년값 (y축) + yUpper: number; // 이상치 상한 당년값 (y축) +} + +export interface ChartRetDto { + dtl: StAnlsDtlResponse; // 분석 상세 파라미터 (mu, r_mid, T_L, T_U 등) + list: ChartBoundaryPointDto[]; // HBR 경계선 곡선 데이터 리스트 (x_prev, y_lower, y_upper) +} diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 6624c3f8a..8fa5dce05 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -76,6 +76,8 @@ export const ENDPOINT = { VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng", ANALYSIS: "/statverify-service/api/v1/stat-verify/analysis", ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst", + ST_ANLS_DTL: "/statverify-service/api/v1/st-anls-dtl", + ST_ANLS_RSLT: "/statverify-service/api/v1/st-anls-rslt", }, }; diff --git a/packages/common/components/src/ui/date-range-picker/index.tsx b/packages/common/components/src/ui/date-range-picker/index.tsx index 1e8cdfe4c..58fa324d7 100644 --- a/packages/common/components/src/ui/date-range-picker/index.tsx +++ b/packages/common/components/src/ui/date-range-picker/index.tsx @@ -10,7 +10,10 @@ import { Button } from "../button"; import { Calendar } from "../calendar"; import { Popover, PopoverContent, PopoverTrigger } from "../popover"; -type DateRangePickerProps = React.HTMLAttributes & +type DateRangePickerProps = Omit< + React.HTMLAttributes, + "defaultValue" +> & React.ComponentProps & { value?: DateRange; defaultValue?: DateRange; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1bb15fae..e39d4b41f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,10 +101,10 @@ importers: devDependencies: eslint: specifier: ^9.0.0 - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) eslint-config-next: specifier: 15.4.0 - version: 15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + version: 15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.6 @@ -1112,6 +1112,9 @@ importers: chart.js: specifier: ^4.4.8 version: 4.5.1 + chartjs-plugin-zoom: + specifier: ^2.2.0 + version: 2.2.0(chart.js@4.5.1) dayjs: specifier: ^1.11.13 version: 1.11.19 @@ -1187,13 +1190,13 @@ importers: version: 5.28.5 eslint: specifier: ^9 - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) eslint-config-next: specifier: 15.1.7 - version: 15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + version: 5.2.0(eslint@9.39.2(jiti@1.21.7)) postcss: specifier: ^8 version: 8.5.6 @@ -3841,6 +3844,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} @@ -4444,6 +4450,11 @@ packages: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} + chartjs-plugin-zoom@2.2.0: + resolution: {integrity: sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==} + peerDependencies: + chart.js: '>=3.2.0' + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -5314,6 +5325,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hammerjs@2.0.8: + resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} + engines: {node: '>=0.8.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -9305,6 +9320,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/hammerjs@2.0.46': {} + '@types/js-cookie@3.0.6': {} '@types/json-schema@7.0.15': {} @@ -10134,6 +10151,12 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 + chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): + dependencies: + '@types/hammerjs': 2.0.46 + chart.js: 4.5.1 + hammerjs: 2.0.8 + check-error@2.1.1: {} chokidar@3.6.0: @@ -10706,6 +10729,26 @@ snapshots: optionalDependencies: source-map: 0.7.6 + eslint-config-next@15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.1.7 + '@rushstack/eslint-patch': 1.15.0 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-config-next@15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.1.7 @@ -10726,26 +10769,6 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-next@15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 15.4.0 - '@rushstack/eslint-patch': 1.15.0 - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7)) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.4.0 @@ -11344,6 +11367,8 @@ snapshots: graceful-fs@4.2.11: {} + hammerjs@2.0.8: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} From 9d7ceb69b1e15646779309194fed31bd10be00ab Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 9 Mar 2026 18:02:04 +0900 Subject: [PATCH 035/113] =?UTF-8?q?chore:=20pre-push=20=ED=9B=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .husky/pre-push | 6 +++++- package.json | 44 ++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 82c73e102..929f5302d 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,7 @@ #!/bin/sh BRANCH=$(git rev-parse --abbrev-ref HEAD) -pnpm turbo run build --filter="...[origin/$BRANCH]" +echo "Branch: $BRANCH" +echo "Changed packages (dry-run):" +pnpm turbo run build --filter="...[origin/$BRANCH]" --dry-run +echo "Starting build..." +pnpm turbo run build --filter="...[origin/$BRANCH]" \ No newline at end of file diff --git a/package.json b/package.json index 5474fef3c..7c0fdf369 100644 --- a/package.json +++ b/package.json @@ -8,28 +8,28 @@ "apps/*", "packages/*" ], - "scripts": { - "build": "turbo run build", - "build:common": "turbo run build --filter=@common/*", - "dev": "turbo run dev", - "build:cm": "turbo run build --filter=cm", - "build:cs": "turbo run build --filter=cs", - "build:sn": "turbo run build --filter=sn", - "build:wr": "turbo run build --filter=wr", - "build:fo": "turbo run build --filter=fo", - "build:dm": "turbo run build --filter=dm", - "build:dm_new": "turbo run build --filter=dm_new", - "dev:cm": "turbo run dev --filter=cm", - "dev:cs": "turbo run dev --filter=cs", - "dev:sn": "turbo run dev --filter=sn", - "dev:wr": "turbo run dev --filter=wr", - "dev:fo": "turbo run dev --filter=fo", - "dev:dm": "turbo run dev --filter=dm", - "dev:dm_new": "turbo run dev --filter=dm_new", - "preview:components": "turbo run storybook --filter=@common/components", - "preinstall": "npx only-allow pnpm", - "prepare": "husky" - }, +"scripts": { + "build": "turbo run build", + "build:common": "turbo run build --filter=@common/*", + "dev": "turbo run dev", + "build:cm": "turbo run build --filter=cm", + "build:cs": "turbo run build --filter=cs", + "build:sn": "turbo run build --filter=sn", + "build:wr": "turbo run build --filter=wr", + "build:fo": "turbo run build --filter=fo", + "build:dm": "turbo run build --filter=dm", + "build:dm_new": "turbo run build --filter=dm_new", + "dev:cm": "turbo run dev --filter=cm", + "dev:cs": "turbo run dev --filter=cs", + "dev:sn": "turbo run dev --filter=sn", + "dev:wr": "turbo run dev --filter=wr", + "dev:fo": "turbo run dev --filter=fo", + "dev:dm": "turbo run dev --filter=dm", + "dev:dm_new": "turbo run dev --filter=dm_new", + "preview:components": "turbo run storybook --filter=@common/components", + "preinstall": "npx only-allow pnpm", + "prepare": "husky" +}, "dependencies": { "chart.js": "^4.4.8", "react": "^19.2.1", From 9ef58ef1530066a26c24456b9c54f3c94568080c Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 10:38:25 +0900 Subject: [PATCH 036/113] =?UTF-8?q?st:=20=EB=A9=94=EB=89=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20road=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95,=20T?= =?UTF-8?q?abDataGrid=20=EC=88=98=EC=A0=95,=20=EC=9D=BC=EB=B0=98=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=EA=B4=80=EB=A6=AC=20=ED=99=94=EB=A9=B4=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/app/(with-base-layout)/layout.tsx | 2 +- apps/st/src/components/module/TabDataGrid.tsx | 611 ++++++++++-------- .../statistics/input/SewagePipeInp.tsx | 105 ++- .../input/SeweragePenetrationRateInp.tsx | 64 +- .../statistics/input/StatsInputStatusInp.tsx | 71 +- .../statistics/input/SubmitDailog.tsx | 2 +- .../GeneralVerificationManagement.tsx | 180 ++++-- apps/st/src/components/type/DataGrid.type.ts | 4 + .../sewagePipe/RegionalFactStatusInpColumn.ts | 153 ++--- .../input/sewagePipe/SewagePipeInpColumn.ts | 17 + .../management/GeneralVerificationColumn.tsx | 62 ++ ...tatistics-input-sew-supply-rate-service.ts | 4 +- .../statistics/api/statistics-service.ts | 11 + .../model/general-verification-interface.ts | 12 + .../statistics/model/statistics-interface.ts | 15 + ...tics-sewage-supply-rate-input-interface.ts | 2 + .../src/shared/ui/statistics-page-layout.tsx | 2 +- apps/st/src/store/tab-store.ts | 2 +- 18 files changed, 829 insertions(+), 490 deletions(-) create mode 100644 apps/st/src/components/util/column/input/sewagePipe/SewagePipeInpColumn.ts create mode 100644 apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx create mode 100644 apps/st/src/entities/statistics/model/general-verification-interface.ts diff --git a/apps/st/app/(with-base-layout)/layout.tsx b/apps/st/app/(with-base-layout)/layout.tsx index 3fe61855e..98c126bf6 100644 --- a/apps/st/app/(with-base-layout)/layout.tsx +++ b/apps/st/app/(with-base-layout)/layout.tsx @@ -76,7 +76,7 @@ export default function WithBaseLayout({ // menuList에서 활성 탭의 메뉴 찾기 const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => { for (const item of items) { - if (item.menuEngNm === activeTabId) { + if (item.urlPath === activeTabId) { return item; } if (item.children) { diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index aa36287ee..cbbe53a0c 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -92,20 +92,20 @@ function toNum(v: unknown): number { // } // } // } - + // if (!gridEl) return; - + // columns.forEach((col) => { // if (!col.fieldName && !col.name) return; - + // const fieldName = col.fieldName || col.name; // const direction = sortDir[fieldName] || sortDir[col.name] || "none"; // if (direction === "none") return; - + // // 헤더 셀 찾기 (fieldName 또는 name으로) // const headerCells = gridEl.querySelectorAll('.rg-header-cell, [class*="header"], .rg-header'); // let targetCell: HTMLElement | null = null; - + // headerCells.forEach((cell) => { // const el = cell as HTMLElement; // const text = el.textContent?.trim(); @@ -121,17 +121,17 @@ function toNum(v: unknown): number { // targetCell = el; // } // }); - + // if (!targetCell) return; - + // const headerCell = targetCell as HTMLElement; - + // // 기존 화살표 제거 // const existingIndicator = headerCell.querySelector(".rg-sort-indicator") as HTMLElement | null; // if (existingIndicator) { // existingIndicator.remove(); // } - + // // 화살표 추가 // const indicator = document.createElement("span"); // indicator.className = "rg-sort-indicator"; @@ -164,7 +164,7 @@ function createOnSortingHandler( const groupField = mergeCellCol?.fieldName || null; const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음 const hasMergeCells = mergeCellCols.length > 0; - + return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) { const grid = g as { getColumn?: (i: number) => { fieldName?: string; name?: string } | undefined; getDataSource?: () => { getRowCount(): number; getValue(r: number, f: string): unknown } }; const idx = fields?.[0]; @@ -178,10 +178,10 @@ function createOnSortingHandler( if (!sortField) return true; const col = columns.find((c) => (c.fieldName === sortField || c.name === sortField)); if (!col || col.sortdisable) return true; - + // mergeCells: true인 컬럼이 없으면 RealGrid 기본 정렬 사용 if (!hasMergeCells || !groupField) return true; - + const ds = grid.getDataSource?.(); if (!ds) return true; const cnt = ds.getRowCount(); @@ -229,7 +229,7 @@ function sortRowsGroupPreserving( if (!groups.has(k)) groups.set(k, []); groups.get(k)!.push(r); } - + // valueType에 따라 비교 함수 선택 const compareValue = (val: unknown): number | string => { if (valueType === ValueType.NUMBER) { @@ -238,7 +238,7 @@ function sortRowsGroupPreserving( // 텍스트나 기타 타입은 문자열로 비교 return String(val ?? ""); }; - + const rep = (arr: Record[]): number | string => { const values = arr.map((r) => compareValue(r[sortField])); if (valueType === ValueType.NUMBER) { @@ -253,7 +253,7 @@ function sortRowsGroupPreserving( return sortAsc ? (strs[0] as string) : (strs[strs.length - 1] as string); } }; - + const entries = [...groups.entries()].sort((a, b) => { const valA = rep(a[1]); const valB = rep(b[1]); @@ -265,7 +265,7 @@ function sortRowsGroupPreserving( const strB = String(valB); return (sortAsc ? 1 : -1) * strA.localeCompare(strB); }); - + return entries.flatMap(([, arr]) => arr); } @@ -306,9 +306,10 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string }; } -const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange }: TabDataGridProps) => { +const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange, getGridDataRef }: TabDataGridProps) => { // 각 탭별로 그리드 ref와 데이터를 관리 const gridRefs = useRef>(new Map()); + const activeTabRef = useRef(""); const fileInputRefs = useRef>(new Map()); // 각 탭별로 정렬 상태 관리 const sortDirRefs = useRef>>>(new Map()); @@ -323,7 +324,8 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }); const isControlled = activeTabProp !== undefined; const activeTab = isControlled ? activeTabProp : internalActiveTab; - const setActiveTab = isControlled ? (onActiveTabChange ?? (() => {})) : setInternalActiveTab; + const setActiveTab = isControlled ? (onActiveTabChange ?? (() => { })) : setInternalActiveTab; + activeTabRef.current = activeTab; const exportExcel = (tabKey: string) => { const currentRowData = rowDataMap.get(tabKey) || []; @@ -345,7 +347,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r const columnInfoItem = columnInfo.find(info => info.tabKey === tabKey); if (!columnInfoItem) return; - + const tabLabel = columnInfoItem.tabLabel || tabKey; const columns = columnInfoItem.columns || []; const columnLayout = columnInfoItem.columnLayout || []; @@ -520,7 +522,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r const columns = columnInfoItem.columns || []; headers.forEach((headerText, colIndex) => { let fieldName: string | null = null; - + // 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)") const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/); if (fieldNameMatch && fieldNameMatch[1]) { @@ -529,13 +531,13 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r fieldName = extractedFieldName; } } - + // 매칭 실패 시 원본 헤더 텍스트 사용 if (!fieldName) { console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`); fieldName = headerText; } - + headerToFieldNameMap.set(headerText, fieldName); }); @@ -656,6 +658,41 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }); }, [rowDataMap, columnInfo]); + // getGridDataRef: 현재 활성 탭 그리드 데이터 반환 함수 할당 + useEffect(() => { + if (!getGridDataRef) return; + getGridDataRef.current = () => { + const tabKey = activeTabRef.current; + const gridRef = gridRefs.current.get(tabKey); + const gv = gridRef?.gridView as + | { + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } + | undefined; + if (!gv?.getDataSource) return []; + const ds = gv.getDataSource(); + if (!ds) return []; + const currentInfo = columnInfo.find((info) => info.tabKey === tabKey); + const columns = currentInfo?.columns ?? []; + const fieldNames = columns + .map((c) => c.fieldName) + .filter((x): x is string => Boolean(x)); + 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; + }; + }, [columnInfo, getGridDataRef]); // 탭 전환 시 그리드 레이아웃·푸터·포맷 재적용 (여러 시점 + ResizeObserver) const appliedTabRef = useRef(null); @@ -728,7 +765,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r if (item.valueCallback && footerArr[i]) (footerArr[i] as FooterItemWithCallback).valueCallback = item.valueCallback; }); - } catch (_) {} + } catch (_) { } }); } columns.forEach(col => { @@ -738,7 +775,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r 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 (_) {} + } catch (_) { } }); if (dp?.setRows && tabRowData.length) dp.setRows(tabRowData); @@ -788,7 +825,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r {columnInfo.map((info) => { const tabKey = info.tabKey; - + // isSplitGrid가 true이고 left, right가 있으면 SplitDataGrid 사용 if (info.isSplitGrid && info.left && info.right) { const leftRowData = rowDataMap.get(info.left.tabKey || `${tabKey}_left`) || []; @@ -886,277 +923,277 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
{activeTab === tabKey ? ( - { - if (el) { - gridRefs.current.set(tabKey, el); - // 그리드가 생성된 직후 fitStyle 설정 - if (el.gridView) { - setTimeout(() => { - const gridView = el.gridView; - if (!gridView) return; - - const currentInfo = columnInfo.find(info => info.tabKey === tabKey); - if (!currentInfo || !currentInfo.columns) return; - - const columns = currentInfo.columns; - - 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 - }); - gridView.header.height = 60; - - // 헤더 체크박스 표시 여부 설정 - gridView.setOptions({ - checkBar: { visible: showHeaderCheckbox }, - rowIndicator: { visible: false }, - }); - - // footer가 있는 컬럼이 있으면 footer 높이 설정 - const hasFooter = columns.some(col => col.footer); - if (hasFooter) { - // footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행) - const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer)); - const hasMultiRowFooter = columns.some(col => { - const footer = col.footer; - return footer && !Array.isArray(footer) && 'multiRow' in footer && footer.multiRow === true; - }); - if (hasArrayFooter || hasMultiRowFooter) { - // footer 배열의 길이를 확인하여 행 수 결정 - const arrayFooterLengths = columns - .filter(col => col.footer && Array.isArray(col.footer)) - .map(col => (col.footer as unknown[]).length); - const maxFooterLength = arrayFooterLengths.length > 0 - ? Math.max(...arrayFooterLengths) - : 2; // 기본값 2 - const footerHeights = Array.from({ length: maxFooterLength }, () => ({ height: 40 })); - gridView.setFooters?.(footerHeights); - } else { - gridView.footer.height = 40; - } - } - - gridView.setOptions({ - summaryMode: "aggregate" - }); - - // footerUserSpans가 있는 경우 setColumnLayout으로 설정 - const checkFooterUserSpans = (layout: unknown[]): boolean => { - return layout.some(item => { - if (typeof item === 'string') return false; - if (item && typeof item === 'object' && 'footerUserSpans' in item) return true; - if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) { - return checkFooterUserSpans(item.items); - } - return false; - }); - }; - - if (checkFooterUserSpans(columnLayout)) { - // setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함 - setTimeout(() => { - if (gridView && columnLayout) { - gridView.setColumnLayout?.(columnLayout); - } - }, 100); - } - - // footer 설정 (배열 또는 단일 객체 모두 처리) - // setFooters 호출 후 footer 배열 설정 + { + if (el) { + gridRefs.current.set(tabKey, el); + // 그리드가 생성된 직후 fitStyle 설정 + if (el.gridView) { setTimeout(() => { - const currentGridView = gridView; - if (!currentGridView || !currentGridView.columnByName) { - console.warn('GridView not available for footer setup'); - return; + const gridView = el.gridView; + if (!gridView) return; + + const currentInfo = columnInfo.find(info => info.tabKey === tabKey); + if (!currentInfo || !currentInfo.columns) return; + + const columns = currentInfo.columns; + + const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record[]) => void } | undefined; + if (dp?.setFields) { + dp.setFields(buildFieldsFromColumns(columns)); } - - // footer가 배열인 컬럼이 있으면 setColumns를 다시 호출하여 footer 설정 - const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer)); - if (hasArrayFooter) { - // 모든 컬럼 정보를 다시 가져와서 footer 배열을 포함하여 setColumns 호출 - const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => { - const colInfo: 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: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), - }; - - if (col.footer) { - if (Array.isArray(col.footer)) { - colInfo.footer = col.footer; - } else { - colInfo.footer = { - text: col.footer.text, - expression: col.footer.expression, - numberFormat: col.footer.numberFormat, - }; - } - } - - return colInfo; + // setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지) + const tabRowData = rowDataMap.get(tabKey) || []; + if (dp?.setRows && tabRowData.length) { + dp.setRows(tabRowData); + } + + gridView.setDisplayOptions({ + fitStyle: GridFitStyle.EVEN + }); + gridView.header.height = 60; + + // 헤더 체크박스 표시 여부 설정 + gridView.setOptions({ + checkBar: { visible: showHeaderCheckbox }, + rowIndicator: { visible: false }, + }); + + // footer가 있는 컬럼이 있으면 footer 높이 설정 + const hasFooter = columns.some(col => col.footer); + if (hasFooter) { + // footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행) + const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer)); + const hasMultiRowFooter = columns.some(col => { + const footer = col.footer; + return footer && !Array.isArray(footer) && 'multiRow' in footer && footer.multiRow === true; }); - - try { - (currentGridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter); - - // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 - setTimeout(() => { - columns.forEach(col => { - if (!col.footer || !Array.isArray(col.footer)) return; - - try { - const column = currentGridView.columnByName(col.name); - if (!column) return; - - // footer 배열의 각 요소에 valueCallback 설정 - col.footer.forEach((footerItem, index) => { - if (footerItem.valueCallback && column.footer) { - // footer가 배열인 경우 - if (Array.isArray(column.footer) && column.footer[index]) { - (column.footer[index] as FooterItemWithCallback).valueCallback = footerItem.valueCallback; - } - } - }); - } catch (e) { - console.warn(`Error setting footer array valueCallback for column ${col.name}:`, e); + if (hasArrayFooter || hasMultiRowFooter) { + // footer 배열의 길이를 확인하여 행 수 결정 + const arrayFooterLengths = columns + .filter(col => col.footer && Array.isArray(col.footer)) + .map(col => (col.footer as unknown[]).length); + const maxFooterLength = arrayFooterLengths.length > 0 + ? Math.max(...arrayFooterLengths) + : 2; // 기본값 2 + const footerHeights = Array.from({ length: maxFooterLength }, () => ({ height: 40 })); + gridView.setFooters?.(footerHeights); + } else { + gridView.footer.height = 40; + } + } + + gridView.setOptions({ + summaryMode: "aggregate" + }); + + // footerUserSpans가 있는 경우 setColumnLayout으로 설정 + const checkFooterUserSpans = (layout: unknown[]): boolean => { + return layout.some(item => { + if (typeof item === 'string') return false; + if (item && typeof item === 'object' && 'footerUserSpans' in item) return true; + if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) { + return checkFooterUserSpans(item.items); + } + return false; + }); + }; + + if (checkFooterUserSpans(columnLayout)) { + // setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함 + setTimeout(() => { + if (gridView && columnLayout) { + gridView.setColumnLayout?.(columnLayout); + } + }, 100); + } + + // footer 설정 (배열 또는 단일 객체 모두 처리) + // setFooters 호출 후 footer 배열 설정 + setTimeout(() => { + const currentGridView = gridView; + if (!currentGridView || !currentGridView.columnByName) { + console.warn('GridView not available for footer setup'); + return; + } + + // footer가 배열인 컬럼이 있으면 setColumns를 다시 호출하여 footer 설정 + const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer)); + if (hasArrayFooter) { + // 모든 컬럼 정보를 다시 가져와서 footer 배열을 포함하여 setColumns 호출 + const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => { + const colInfo: 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: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), + }; + + if (col.footer) { + if (Array.isArray(col.footer)) { + colInfo.footer = col.footer; + } else { + colInfo.footer = { + text: col.footer.text, + expression: col.footer.expression, + numberFormat: col.footer.numberFormat, + }; } - }); - }, 50); - } catch (e) { - console.warn('Failed to set columns with footer array:', e); - } - } - - // footer가 단일 객체이고 valueCallback이 있는 경우 - columns.forEach(col => { - if (!col.footer || Array.isArray(col.footer)) return; - - try { - const column = currentGridView.columnByName(col.name); - if (!column || !column.footer) return; - - const footerObj = col.footer as { valueCallback?: (grid: unknown, column: unknown, footerIndex: number, columnFooter: unknown, value: unknown) => unknown }; - if (footerObj.valueCallback) { - (column.footer as { valueCallback?: typeof footerObj.valueCallback }).valueCallback = footerObj.valueCallback; - } - } catch (e) { - console.warn(`Error setting footer valueCallback for column ${col.name}:`, e); - } - }); - }, 200); - - // 고정 컬럼 설정 - if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) { - gridView.setFixedOptions({ - colCount: currentInfo.fixedColumns, - }); - } - - // 각 탭별 ref 초기화 - if (!sortDirRefs.current.has(tabKey)) { - sortDirRefs.current.set(tabKey, { current: {} }); - } - if (!originalRowsRefs.current.has(tabKey)) { - originalRowsRefs.current.set(tabKey, { current: {} }); - } - - const sortDirRef = sortDirRefs.current.get(tabKey)!; - const originalRowsRef = originalRowsRefs.current.get(tabKey)!; - - // mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리 - const hasMergeCells = columns.some((col) => col.mergeCells === true); - - // setDataComparer로 숫자 정렬 (mergeCells가 있으면 onSorting에서 클라이언트 정렬로 처리) - if (el.dataProvider) { - const dp = el.dataProvider as RealGridDataProviderComparer; - if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") { - const numberComparer = createNumberComparer(dp); - const seen = new Set(); - columns.forEach((col) => { - if (hasMergeCells) return; // mergeCells가 있으면 모든 컬럼을 클라이언트 정렬로 처리 - if (col.valueType === ValueType.NUMBER && col.fieldName && !seen.has(col.fieldName)) { - seen.add(col.fieldName); - try { - dp.setDataComparer!(col.fieldName, numberComparer); - } catch (e) { - console.warn("setDataComparer:", e); } + + return colInfo; + }); + + try { + (currentGridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter); + + // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 + setTimeout(() => { + columns.forEach(col => { + if (!col.footer || !Array.isArray(col.footer)) return; + + try { + const column = currentGridView.columnByName(col.name); + if (!column) return; + + // footer 배열의 각 요소에 valueCallback 설정 + col.footer.forEach((footerItem, index) => { + if (footerItem.valueCallback && column.footer) { + // footer가 배열인 경우 + if (Array.isArray(column.footer) && column.footer[index]) { + (column.footer[index] as FooterItemWithCallback).valueCallback = footerItem.valueCallback; + } + } + }); + } catch (e) { + console.warn(`Error setting footer array valueCallback for column ${col.name}:`, e); + } + }); + }, 50); + } catch (e) { + console.warn('Failed to set columns with footer array:', e); + } + } + + // footer가 단일 객체이고 valueCallback이 있는 경우 + columns.forEach(col => { + if (!col.footer || Array.isArray(col.footer)) return; + + try { + const column = currentGridView.columnByName(col.name); + if (!column || !column.footer) return; + + const footerObj = col.footer as { valueCallback?: (grid: unknown, column: unknown, footerIndex: number, columnFooter: unknown, value: unknown) => unknown }; + if (footerObj.valueCallback) { + (column.footer as { valueCallback?: typeof footerObj.valueCallback }).valueCallback = footerObj.valueCallback; + } + } catch (e) { + console.warn(`Error setting footer valueCallback for column ${col.name}:`, e); } }); + }, 200); + + // 고정 컬럼 설정 + if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) { + gridView.setFixedOptions({ + colCount: currentInfo.fixedColumns, + }); } - } - - // mergeCells: true인 컬럼이 있으면 onSorting에서 그룹 내 순서 유지 정렬 적용 - const setRowData = (updater: React.SetStateAction[]>) => { - setRowDataMap(prev => { - const newMap = new Map(prev); - const currentData = newMap.get(tabKey) || []; - const newData = typeof updater === 'function' ? updater(currentData) : updater; - newMap.set(tabKey, newData); - return newMap; - }); - }; - - // 그리드 DOM 요소 찾기 (나중에 화살표 업데이트에 사용) - // 탭별로 그리드 요소를 찾기 위해 data-tab-key 사용 - const findGridElement = () => { - const tabContent = document.querySelector(`[data-tab-key="${tabKey}"]`); - return tabContent?.querySelector('.rg-grid, [data-grid-id]') as HTMLElement | null; - }; - - const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement); - (gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler; - }, 150); + + // 각 탭별 ref 초기화 + if (!sortDirRefs.current.has(tabKey)) { + sortDirRefs.current.set(tabKey, { current: {} }); + } + if (!originalRowsRefs.current.has(tabKey)) { + originalRowsRefs.current.set(tabKey, { current: {} }); + } + + const sortDirRef = sortDirRefs.current.get(tabKey)!; + const originalRowsRef = originalRowsRefs.current.get(tabKey)!; + + // mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리 + const hasMergeCells = columns.some((col) => col.mergeCells === true); + + // setDataComparer로 숫자 정렬 (mergeCells가 있으면 onSorting에서 클라이언트 정렬로 처리) + if (el.dataProvider) { + const dp = el.dataProvider as RealGridDataProviderComparer; + if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") { + const numberComparer = createNumberComparer(dp); + const seen = new Set(); + columns.forEach((col) => { + if (hasMergeCells) return; // mergeCells가 있으면 모든 컬럼을 클라이언트 정렬로 처리 + if (col.valueType === ValueType.NUMBER && col.fieldName && !seen.has(col.fieldName)) { + seen.add(col.fieldName); + try { + dp.setDataComparer!(col.fieldName, numberComparer); + } catch (e) { + console.warn("setDataComparer:", e); + } + } + }); + } + } + + // mergeCells: true인 컬럼이 있으면 onSorting에서 그룹 내 순서 유지 정렬 적용 + const setRowData = (updater: React.SetStateAction[]>) => { + setRowDataMap(prev => { + const newMap = new Map(prev); + const currentData = newMap.get(tabKey) || []; + const newData = typeof updater === 'function' ? updater(currentData) : updater; + newMap.set(tabKey, newData); + return newMap; + }); + }; + + // 그리드 DOM 요소 찾기 (나중에 화살표 업데이트에 사용) + // 탭별로 그리드 요소를 찾기 위해 data-tab-key 사용 + const findGridElement = () => { + const tabContent = document.querySelector(`[data-tab-key="${tabKey}"]`); + return tabContent?.querySelector('.rg-grid, [data-grid-id]') as HTMLElement | null; + }; + + const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement); + (gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler; + }, 150); + } + } else { + gridRefs.current.delete(tabKey); } - } else { - gridRefs.current.delete(tabKey); - } - }} - autoGenerateField={true} - layout={columnLayout} - rows={currentRowData} - > - {columns.map((column) => ( - - ))} - + }} + autoGenerateField={true} + layout={columnLayout} + rows={currentRowData} + > + {columns.map((column) => ( + + ))} + ) : null}
diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 4eac16126..20bed7f23 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1,31 +1,87 @@ -import { useState } from "react"; +import { useState, useMemo } from "react"; import SearchForm from "@/components/module/SearchForm"; -import { TabDataGrid } from '@rsone/components'; +import TabDataGrid from "@/components/module/TabDataGrid"; import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; -import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; -import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn"; -import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn"; -import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn"; -import { ConnectionStatusInpColumn } from "@/components/util/column/input/sewagePipe/ConnectionStatusInpColumn"; +import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; +import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { useAlertStore } from "@/shared/lib"; +import { useAdminStore } from "@/store/admin-store"; -// 하수도 통계 관리 > 02. 하수관로 +// 하수도 통계 관리 > 02. 하수관로 const SewagePipeInp = () => { - - // tab List - const SewagePipeInpTapList = [ - RegionalFactStatusInpColumn, - FactInstallStatusInpColumn, - PipeTypeStatusInpColumn, - PipeDiameterStatusInpColumn, - ManholeStatusInpColumn, - ConnectionStatusInpColumn, - ]; + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const { setMessage: alert } = useAlertStore((state) => state); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"); + const [isEnterable, setIsEnterable] = useState(true); + const { isAdminInfo } = useAdminStore(); + + const columnInfo = useMemo(() => { + const canEdit = isEnterable || isAdminInfo; + return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => { + const applyEditable = (cols: ColumnConfig[] | undefined) => + (cols ?? []).map((col: ColumnConfig) => + canEdit + ? col + : { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" } + ); + + if (tab.left && tab.right) { + return { + ...tab, + left: { ...tab.left, columns: applyEditable(tab.left.columns) }, + right: { ...tab.right, columns: applyEditable(tab.right.columns) }, + }; + } + return { + ...tab, + columns: applyEditable(tab.columns), + }; + }); + }, [isEnterable, isAdminInfo]); + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === 'ALL') { + alert('시도를 선택해주세요.'); + return; + } + + setActiveTab('ST00020001'); + + setRowDataMap(new Map()); + + try { + setIsLoading(true); + + // const searchParam: SewagePipeReviewRequestParams = { + // statsYr: values.year, + // sewerBzmnCd: values.bsnmCd, + // ctpvNm: values.sido, + // sggNm: values.gugun === 'ALL' ? '' : values.gugun + // }; + + // searchParamRef.current = searchParam; + // const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); + + // if (response?.status === 'success' && response.data) { + // const newRowDataMap = new Map[]>(); + // newRowDataMap.set('ST00020001', toGridRows(response.data as object[])); + // setRowDataMap(newRowDataMap); + // } else { + // alert(response?.message || "데이터가 없습니다."); + // } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; const SubmitExceptionFunc = () => { console.log('submit exception'); @@ -47,7 +103,6 @@ const SewagePipeInp = () => { // 저장 로직 }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); return ( <> @@ -60,10 +115,12 @@ const SewagePipeInp = () => {
- ); diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index cf6b184b4..6c660e77b 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -14,7 +14,13 @@ import SubmitDailog from "./SubmitDailog"; import { useAlertStore } from "@/shared/lib"; import { useAdminStore } from "@/store/admin-store"; -import { StatisticsInputSewSupplyRateApi } from "@/entities/statistics/api/statistics-input-sew-supply-rate-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/statistics-input-sew-supply-rate-service"; +import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +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"; + import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SewerageSupplyRateItem, @@ -24,14 +30,12 @@ import type { SewerageSupplyRateSaveRequest, SewerageSupplyRateStatusInfo, } from "@/entities/statistics/model/statistics-sewage-supply-rate-input-interface"; +import type { cancelSubmitRequest } from "@/entities/statistics/model/statistics-interface"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; 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"; import { doList } from "@/components/util/column/input/DoList"; + type GridRowWithVerification = Record & { verificationResult?: SewerageSupplyRateVerificationResult; }; @@ -60,11 +64,12 @@ const SeweragePenetrationRateInp = () => { const [lastSearchedValues, setLastSearchedValues] = useState(null); const [statusInfo, setStatusInfo] = useState(null); const [showDailog, setShowDailog] = useState(false); + const [statsCd, setStatsCd] = useState(''); + const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); - const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); const getGridDataRef = useRef<(() => Record[]) | null>(null); const _footerFieldNames = ['sewerBzmnWholPpltnCnt', 'sewerBzmnDomplWholPpltnCnt', 'sewerBzmnFrgnrWholPpltnCnt', 'sewerBzmnArea'] as const; @@ -103,7 +108,7 @@ const SeweragePenetrationRateInp = () => { const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => { try { setIsLoading(true); - const response = await StatisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params); + const response = await statisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params); if (response.status === 'success' && response.data) { const commonInfo = response.data.commonInfo; const statusInfo = response.data.statusInfo; @@ -121,7 +126,7 @@ const SeweragePenetrationRateInp = () => { }) ); - setStatusInfo(response.data.statusInfo); + setStatusInfo(statusInfo); setFooterFromApi( commonInfo ? { @@ -132,7 +137,7 @@ const SeweragePenetrationRateInp = () => { } : null ); - + setStatsCd(commonInfo.statsCd); if (statusInfo && statusInfo.laeSbmsnSttsCd) { if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) { setIsEnterable(false); @@ -163,7 +168,7 @@ const SeweragePenetrationRateInp = () => { } try { - const response = await StatisticsInputSewSupplyRateApi.checkInputPeriod(values.year || ''); + const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || ''); if (response.status === 'success') { const checkEnterable = response.data; @@ -266,6 +271,7 @@ const SeweragePenetrationRateInp = () => { const requestData: SewerageSupplyRateSaveRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', ctpvNm: searchValues?.sido ?? '', + sggNm: searchValues?.gugun ?? '', statsYr: searchValues?.year ?? '', userId: userInfo.userUnqId, submitList: convertedData, @@ -280,7 +286,7 @@ const SeweragePenetrationRateInp = () => { laeIdfrTelNo: submitInfo.laeIdfrTelNo, }; - const response = await StatisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); + const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); alert('제출되었습니다.'); @@ -368,6 +374,7 @@ const SeweragePenetrationRateInp = () => { const requestData: SewerageSupplyRateSaveRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드 ctpvNm: searchValues?.sido ?? '', // 시도 + sggNm: searchValues?.gugun ?? '', statsYr: searchValues?.year ?? '', // 통계 연도 userId: userInfo.userUnqId, // 사용자 saveList: convertedData, // 저장 데이터 @@ -378,7 +385,7 @@ const SeweragePenetrationRateInp = () => { requestData.superAdmin = 'superAdmin'; } - const response = await StatisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); + const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); if (response.status === 'success') { fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); @@ -395,7 +402,36 @@ const SeweragePenetrationRateInp = () => { setIsLoading(false); } }; - console.log(footerFromApi); + + const cnacelSubmitFunc = async () => { + try { + if (await confirm('제출된 내용을 취소하시겠습니까?')) { + + const userInfo = await getUserInfo(); + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: searchValues?.bsnmCd ?? '', + ctpvNm: searchValues?.sido ?? '', + sggNm: searchValues?.gugun ?? '', + statsYr: searchValues?.year ?? '', + userId: userInfo.userUnqId, + statsCd: statsCd, + }; + + const response = await statisticsAPi.cancelSubmit(requestData); + + if (response.status === 'success') { + fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); + alert('제출 취소되었습니다.'); + } else { + alert(response.message ?? '제출 취소 중 오류가 발생했습니다.'); + } + } + } catch { + alert('제출 취소 중 오류가 발생했습니다.'); + } + }; + return ( <>
@@ -411,7 +447,7 @@ const SeweragePenetrationRateInp = () => { )} - {statusInfo?.laeSbmsnSttsCd === '2' && console.log('test')} />} + {statusInfo?.laeSbmsnSttsCd === '2' && } {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && }
diff --git a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx index e6e87c207..4390777b9 100644 --- a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx +++ b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx @@ -1,7 +1,8 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import useSWR, { useSWRConfig } from "swr"; import SearchForm from "@/components/module/SearchForm"; -// import { DataGrid } from "@rsone/components"; +import { useTabStore } from "@/store/tab-store"; import DataGrid from "@/components/module/DataGrid"; import ApproveButton from "@/components/button/ApproveButton"; import RejectButton from "@/components/button/RejectButton"; @@ -13,45 +14,63 @@ import type { StatsInputStatusListRequestParams } from "@/entities/statistics/mo import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { useAlertStore } from "@/shared/lib"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { StatsInputStatusListApiResponse } from "@/entities/statistics/model/statistics-input-status-interface"; + +const getStatsInputStatusFetcher = async (key: [string, StatsInputStatusListRequestParams]): Promise => { + return StatisticsInputStatusApi.getStatisticsInputStatusList(key[1]); +}; // 하수도 통계 관리 > 00. 통계 입력 현황 const StatsInputStatusInp = () => { const { setMessage: alert } = useAlertStore((state) => state); + const activeTabId = useTabStore((state) => state.activeTabId); const [rowData, setRowData] = useState[]>([]); - const [isLoading, setIsLoading] = useState(false); + const [searchParams, setSearchParams] = useState(null); - const handleSearch = async (values: SearchFormValues) => { - if (values.sido === '' || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); + const swrKey = searchParams ? (["stats-input-status", searchParams] as const) : null; + const { mutate } = useSWRConfig(); + const { isLoading } = useSWR( + swrKey, + getStatsInputStatusFetcher, + { + revalidateOnFocus: true, + onSuccess: (data) => { + if (data.status === "success" && data.data) { + setRowData(toGridRows(data.data)); + } else { + alert(data.message ?? "조회에 실패했습니다."); + } + }, + onError: () => { + alert("데이터 조회 중 오류가 발생했습니다."); + setRowData([]); + }, + }, + ); + + useEffect(() => { + if (activeTabId === 'StatsInputStatusInp' && swrKey) { + mutate(swrKey); + } + }, [activeTabId]); + + const handleSearch = (values: SearchFormValues) => { + if (values.sido === "" || values.sido === "ALL") { + alert("시도를 선택해주세요."); return; } - - const searchParam: StatsInputStatusListRequestParams = { + const params: StatsInputStatusListRequestParams = { ctpvNm: values.sido, sewerBzmnCd: values.bsnmCd, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sggNm: values.gugun === "ALL" ? "" : values.gugun, statsYr: values.year, }; - - try { - setIsLoading(true); - - const response = await StatisticsInputStatusApi.getStatisticsInputStatusList(searchParam); - console.log(response) - if (response.status === 'success' && response.data) { - setRowData(toGridRows(response.data)); - } else { - alert(response.message); - } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - setRowData([]); - } finally { - setIsLoading(false); - } + setSearchParams(params); + mutate(["stats-input-status", params]); }; const handleReset = () => { + setSearchParams(null); setRowData([]); }; diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx index b286117b7..7b67d4cb7 100644 --- a/apps/st/src/components/statistics/input/SubmitDailog.tsx +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -155,7 +155,7 @@ const SubmitDailog = ({ open, onOpenChange, statusInfo, onConfirm }: SubmitDialo 취소 diff --git a/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx b/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx index 0bd348123..16947f896 100644 --- a/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx +++ b/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx @@ -1,79 +1,141 @@ "use client"; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { CodeSelect } from '@/entities/code'; import { Button, Label, - Table, - TableBody, - TableCell, - TableHead, - TableRow, + // Table, + // TableBody, + // TableCell, + // TableHead, + // TableRow, + DataTable, } from "@common/components"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +// import { useDataTable } from "@/shared/lib"; +import { GeneralVerificationColumn } from '@/components/util/column/management/GeneralVerificationColumn'; +import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface'; + const GeneralVerificationManagement = () => { const [statsCd, setStatsCd] = useState(''); + // const [rowSelection, setRowSelection] = useState({}); + // const [pagination, setPagination] = useState({ + // pageIndex: 0, + // pageSize: 100, + // }); + + // const { tableComponent } = useDataTable({ + // tableOptions: { + // GeneralVerificationColumn, + // state: { + // pagination, + // rowSelection, + // }, + // manualPagination: true, + // enableMultiRowSelection: true, + // getCoreRowModel: getCoreRowModel(), + // onPaginationChange: setPagination, + // onRowSelectionChange: setRowSelection, + // }, + // // dataTableOptions: { + // // onRowClick: handleRowClick, + // // viewPagination: true, + // // }, + // }); + + const mockData = useMemo( + () => [ + { + vrfSn: 1, + tabNm: '하수도보급률', + vrfNm: '인구수 검증', + vrfClsCd: '시', + statsCd: 'ST000001', + useYn: 'Y', + rgtrId: '관리자', + regDt: '20260102', + }, + { + vrfSn: 2, + tabNm: '하수도보급률', + vrfNm: '인구수 검증', + vrfClsCd: '시', + statsCd: 'ST000001', + useYn: 'Y', + rgtrId: '관리자', + regDt: '20260102', + }, + { + vrfSn: 3, + tabNm: '하수도보급률', + vrfNm: '인구수 검증', + vrfClsCd: '시', + statsCd: 'ST000001', + useYn: 'Y', + rgtrId: '관리자', + regDt: '20260102', + }, + { + vrfSn: 4, + tabNm: '하수도보급률', + vrfNm: '인구수 검증', + vrfClsCd: '시', + statsCd: 'ST000001', + useYn: 'Y', + rgtrId: '관리자', + regDt: '20260102', + }, + ], + [] + ); + + const table = useReactTable({ + data: mockData, + columns: GeneralVerificationColumn, + getCoreRowModel: getCoreRowModel(), + }); + + const handleSearch = () => { + console.log(statsCd); + }; return ( -
+
-
-
-
- - -
-
- - -
+
+
+ +
- +
+ + +
+
-
+
- - - No - 검증항목 - 등록자 - 등록일 - 사용여부 - - - - - 1 - - - 인구수검증 - - - 관리자 - - - 2026-02-09 - - - Y - - - -
+
+

일반 검증 목록

+
+
+ +
diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index d9bf1997c..1423c7940 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -262,6 +262,10 @@ export type TabDataGridProps = { activeTab?: string; /** 활성 탭 변경 시 호출 (제어 모드). activeTab과 함께 넘기면 부모가 탭 상태를 가짐 */ onActiveTabChange?: (tabKey: string) => void; + /** 저장 등 시점에 현재 활성 탭 그리드 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */ + getGridDataRef?: { + current: (() => Record[]) | null; + }; }; export type SplitDataGridProps = { diff --git a/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts index a02919150..50c16fe40 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts @@ -6,92 +6,97 @@ import { ValueType } from "realgrid"; export const RegionalFactStatusInpColumn = { columns: [ // 행정구역 - { name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' }, editable: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, // 기본 정보 - { name: 'targetYr', fieldName: '하수정비기본계획목표년도', header: { text: '하수정비기본계획목표년도' }}, - { name: 'piplImprnPlanExtn', fieldName: '총관로정비계획연장(m)', header: { text: '총관로정비계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'piplPlanExtn', fieldName: '총계획연장(m)', header: { text: '총계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'piplFcltyExtn', fieldName: '총시설연장(m)', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'piplSupprt', fieldName: '하수관로보급률(%)', header: { text: '하수관로보급률(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정비기본계획목표년도' }}, + { name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 합류식(m) - { name: 'cmbnPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'cmbnImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnFcltyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnRctglExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnCicleExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnOpswrExtn', fieldName: '개거(m)', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cmbnSdgttrExtn', fieldName: '측구(m)', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, // 분류식(m) > 오수관로 - { name: 'pltwtrPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'pltwtrImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'pltwtrPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'pltwtrFlctyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'pltwtrSqreExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'pltwtrCiclExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, // 분류식(m) > 우수관로 - { name: 'rainwtrPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, - { name: 'rainwtrImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrFlctyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrSqreExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrCiclExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrOpswrExtn', fieldName: '개거(m)', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'rainwtrSdgttrExtn', fieldName: '측구(m)', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, // 합류식맨홀(개소) - { name: 'mnhleSite', fieldName: '합류식맨홀(개소)', header: { text: '합류식맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 분류식 > 오수맨홀, 우수맨홀 - { name: 'pltwtrMnhleSite', fieldName: '오수맨홀(개소)', header: { text: '오수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwtrMnhleSite', fieldName: '우수맨홀(개소)', header: { text: '우수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'pltwtrRcvrSite', fieldName: '오수받이(개소)', header: { text: '오수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwtrRcvrSite', fieldName: '빗물받이(개소)', header: { text: '빗물받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'soilspllSite', fieldName: '우수토실(개소)', header: { text: '우수토실(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'soilholeSite', fieldName: '우수토구(개소)', header: { text: '우수토구(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수도대장 전산화 - { name: 'sewerRegstrCmptExtn', fieldName: '하수도대장전산화연장(m)', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewerRegstrCmptRt', fieldName: '하수도대장전산화율(%)', header: { text: '하수도대장전산화율(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'rmk', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, ], columnLayout: [ { name: 'adminArea', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['sido', 'sgg'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] }, + 'sggNm', + 'admdstNm', + ], }, - 'targetYr', - 'piplImprnPlanExtn', - 'piplPlanExtn', - 'piplFcltyExtn', - 'piplSupprt', + 'goalYr', + 'wholPplMtncPlanPrlg', + 'wholPlanPrlg', + 'wholFcltPrlg', + 'swagePplPsplyRt', { name: 'combined', hideChildHeaders: false, header: { text: '합류식(m)' }, items: [ - 'cmbnPlanAr', - 'cmbnImprnPlanExtn', - 'cmbnPlanExtn', - 'cmbnFcltyExtn', + 'cbsysPplMtncPlanArea', + 'cbsysPplMtncPlanPrlg', + 'cbsysPlanPrlg', + 'cbsysFcltPrlg', { name: 'cmbnCulvert', hideChildHeaders: false, header: { text: '암거' }, - items: ['cmbnRctglExtn', 'cmbnCicleExtn'], + items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'], }, - 'cmbnOpswrExtn', - 'cmbnSdgttrExtn', + 'cbsysOpnPrlg', + 'cbsysGttrPrlg', ], }, { @@ -104,15 +109,15 @@ export const RegionalFactStatusInpColumn = { hideChildHeaders: false, header: { text: '오수관로' }, items: [ - 'pltwtrPlanAr', - 'pltwtrImprnPlanExtn', - 'pltwtrPlanExtn', - 'pltwtrFlctyExtn', + 'drtwPplMtncPlanArea', + 'drtwPplMtncPlanPrlg', + 'drtwPplPlanPrlg', + 'drtwPplFcltPrlg', { name: 'pltwtrCulvert', hideChildHeaders: false, header: { text: '암거' }, - items: ['pltwtrSqreExtn', 'pltwtrCiclExtn'], + items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'], }, ], }, @@ -121,39 +126,39 @@ export const RegionalFactStatusInpColumn = { hideChildHeaders: false, header: { text: '우수관로' }, items: [ - 'rainwtrPlanAr', - 'rainwtrImprnPlanExtn', - 'rainwtrPlanExtn', - 'rainwtrFlctyExtn', + 'exlnPplMtncPlanArea', + 'exlnPplMtncPlanPrlg', + 'exlnPplPlanPrlg', + 'exlnPplFcltPrlg', { name: 'rainwtrCulvert', hideChildHeaders: false, header: { text: '암거' }, - items: ['rainwtrSqreExtn', 'rainwtrCiclExtn'], + items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'], }, - 'rainwtrOpswrExtn', - 'rainwtrSdgttrExtn', + 'exlnPplOpnPrlg', + 'exlnPplGttrPrlg', ], }, ], }, - 'mnhleSite', + 'cbsysMnhlCnt', { name: 'separatedManhole', hideChildHeaders: false, header: { text: '분류식' }, - items: ['pltwtrMnhleSite', 'rainwtrMnhleSite'], + items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'], }, - 'pltwtrRcvrSite', - 'rainwtrRcvrSite', - 'soilspllSite', - 'soilholeSite', - 'sewerRegstrCmptExtn', - 'sewerRegstrCmptRt', - 'rmk', + 'drtwRcvprpCnt', + 'exlnRcvprpCnt', + 'stofcbrCnt', + 'exlnOtflCnt', + 'sewerLdgrEdpPrlg', + 'sewerLdgrEdpRt', + 'rmrk', ], rowStartNum: 4, fixedColumns: 1, - tabKey: 'regionalFactStatusInp', + tabKey: 'ST00020001', tabLabel: '지역별 시설현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewagePipe/SewagePipeInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/SewagePipeInpColumn.ts new file mode 100644 index 000000000..9030d4700 --- /dev/null +++ b/apps/st/src/components/util/column/input/sewagePipe/SewagePipeInpColumn.ts @@ -0,0 +1,17 @@ +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; +import { RegionalFactStatusInpColumn } from "./RegionalFactStatusInpColumn"; +import { FactInstallStatusInpColumn } from "./FactInstallStatusInpColumn"; +import { PipeTypeStatusInpColumn } from "./PipeTypeStatusInpColumn"; +import { PipeDiameterStatusInpColumn } from "./PipeDiameterStatusInpColumn"; +import { ManholeStatusInpColumn } from "./ManholeStatusInpColumn"; +import { ConnectionStatusInpColumn } from "./ConnectionStatusInpColumn"; + +/** 하수관로 입력 탭 컬럼 리스트 (TabDataGrid columnInfo용) */ +export const SewagePipeInpColumnInfoList: ColumnInfoList[] = [ + RegionalFactStatusInpColumn, + FactInstallStatusInpColumn, + PipeTypeStatusInpColumn, + PipeDiameterStatusInpColumn, + ManholeStatusInpColumn, + ConnectionStatusInpColumn, +]; diff --git a/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx b/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx new file mode 100644 index 000000000..b61ad9ea3 --- /dev/null +++ b/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx @@ -0,0 +1,62 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@common/components"; +import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface"; + +export const GeneralVerificationColumn: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const { pageIndex, pageSize } = table.getState().pagination; + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "vrfSn", + header: "일련번호", + enableHiding: true, + }, + { + accessorKey: "tabNm", + header: "통계항목", + }, + { + accessorKey: "vrfNm", + header: "검증명", + }, + { + accessorKey: "vrfClsCd", + header: "검증분류", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => ( +
e.stopPropagation()}> + +
+ ), + }, +]; + +const GeneralVerificationManageButton = ({ vrfSn }: { vrfSn: number }) => { + const [dialogOpen, setDialogOpen] = useState(false); + return ( + + ); +}; diff --git a/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts index ba6d10874..7e4e23370 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-sew-supply-rate-service.ts @@ -21,7 +21,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe public async checkInputPeriod(params: string): Promise { const response = await stClient.get( - `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period?year=${params}`, + `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/check-input-period?statsYr=${params}`, ); return response.data; } @@ -43,4 +43,4 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe } } -export const StatisticsInputSewSupplyRateApi = new StatisticsInputSewSupplyRateApiService(); \ No newline at end of file +export const statisticsInputSewSupplyRateApi = new StatisticsInputSewSupplyRateApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index f75163e5c..82ed85b83 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -6,6 +6,8 @@ import { type SearchCondition, type ApiResponse, type Sgg, + type cancelSubmitRequest, + type cancelSubmitResponse, } from "../model/statistics-interface"; export class StatisticsAPiService implements StatisticsService { @@ -23,6 +25,15 @@ export class StatisticsAPiService implements StatisticsService { ); return response.data; } + + // 입력 - 제출 취소 + public async cancelSubmit(param: cancelSubmitRequest): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUTCOMMON}/cancel-submit`, + param + ); + return response.data; + } } export const statisticsAPi = new StatisticsAPiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/general-verification-interface.ts b/apps/st/src/entities/statistics/model/general-verification-interface.ts new file mode 100644 index 000000000..c22504827 --- /dev/null +++ b/apps/st/src/entities/statistics/model/general-verification-interface.ts @@ -0,0 +1,12 @@ +export interface GeneralverificationList { + vrfSn: number; + tabNm: string; + vrfNm: string; + vrfClsCd: string; + statsCd: string; + useYn: string; + rgtrId?: string; + regDt?: string; + mdfrId?: string; + mdfcnDt?: string; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index b909eba92..edf5d264c 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -15,6 +15,20 @@ export interface Sgg { bsnmCd: string; } +export interface cancelSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + statsCd: string; + userId: string; +} + +export interface cancelSubmitResponse { + status: string; + message?: string; +} + export interface ApiResponse { status: string; data: T; @@ -24,4 +38,5 @@ export interface ApiResponse { export interface StatisticsService { getSidoList(): Promise>; getSggList(sidoName: string): Promise>; + cancelSubmit(param: cancelSubmitRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts index 5c1a1f6ef..b3f97fbb1 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-supply-rate-input-interface.ts @@ -29,6 +29,7 @@ export interface SewerageSupplyRateCommonInfo { swagePplPsplyRt: number; ctpvNm: string; statsYr: string; + statsCd: string; } /** 검증 결과 한 건 (필드별 메시지) */ @@ -132,6 +133,7 @@ export interface SewerageSupplyRateSaveItem { export interface SewerageSupplyRateSaveRequest { statsYr: string; ctpvNm: string; + sggNm: string; sewerBzmnCd: string; userId: string; sewerBzmnFrgnrWholPpltnCnt?: number; diff --git a/apps/st/src/shared/ui/statistics-page-layout.tsx b/apps/st/src/shared/ui/statistics-page-layout.tsx index b34ebb5eb..d7d5b98b4 100644 --- a/apps/st/src/shared/ui/statistics-page-layout.tsx +++ b/apps/st/src/shared/ui/statistics-page-layout.tsx @@ -5,7 +5,7 @@ import { TabBar } from "./tab-bar"; function StatisticsPageLayout({ children }: { children: React.ReactNode }) { const { activeTabId, tabs } = useTabStore(); - const pageTitle = tabs.find((t) => t.menuItem.menuEngNm === activeTabId); + const pageTitle = tabs.find((t) => t.menuItem.urlPath === activeTabId); const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim()); diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 6c48a5074..ace855808 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -28,7 +28,7 @@ const useTabStoreHook = create((set, get) => ({ menuList: null, addTab: (menuItem: MenuItem) => { const { tabs, menuList } = get(); - const tabId = menuItem.menuEngNm || ''; + const tabId = menuItem.urlPath || ''; // 이미 존재하는 탭인지 확인 const existingTab = tabs.find((tab) => tab.id === tabId); From 3a1b22b57b4b673fe0ce27c64ea03156e3418a58 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 10:52:27 +0900 Subject: [PATCH 037/113] =?UTF-8?q?refactor:05.=ED=8E=8C=ED=94=84=EC=9E=A5?= =?UTF-8?q?=20=ED=91=9C=EC=A4=80=ED=99=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/PumpingStationRvw.tsx | 38 ++- .../column/review/PumpingStationRvwColumn.ts | 272 +++++++++--------- .../api/statistics-management-service.ts | 45 +-- ...istics-pumping-station-review-interface.ts | 118 ++++---- 4 files changed, 233 insertions(+), 240 deletions(-) diff --git a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx index 3fc34a034..346a9ef33 100644 --- a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx +++ b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx @@ -12,12 +12,15 @@ import { PumpingStationInfoList } from "@/components/util/column/review/PumpingS import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { MediationPumpStationItem } from "@/entities/statistics/model/statistics-pumping-station-review-interface"; -// 하수도 통계 검토 05.중계펌프장 +/** + * 하수도 통계 검토 05.중계펌프장 + */ const PumpingStationRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowData, setRowData] = useState[]>([]); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const columnInfo = useMemo(() => { const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList; @@ -27,7 +30,7 @@ const PumpingStationRvw = () => { rowStartNum, columns: columns.map((col) => ({ ...col, - editable: false as const, // 검토 화면이므로 읽기 전용 + editable: false as const, styleName: col.styleName ?? ("rg-cell-readonly" as const), })), }; @@ -38,7 +41,7 @@ const PumpingStationRvw = () => { }; const handleSearch = async (values: SearchFormValues) => { - if (values.sido === '' || values.sido === 'ALL') { + if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); return; } @@ -47,15 +50,22 @@ const PumpingStationRvw = () => { setIsLoading(true); const response = await statisticsManagementApi.getMediationPumpStationList({ - sido: values.sido, - gugun: values.gugun === 'ALL' ? '' : values.gugun, - bsnmCd: values.bsnmCd, - yr: values.year, - bscFctNm: values.fctName, // 시설명 검색 + ctpvNm: values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + pumpstNm: values.fctName, }); if (response.status === 'success' && response.data) { - setRowData(toGridRows(response.data)); + const rawList = response.data as MediationPumpStationItem[]; + + const convertedData = rawList.map(item => ({ + ...item, + rmrk: item.rmrk || '' + })); + + setRowData(toGridRows(convertedData)); } else { setRowData([]); alert(response.message || "데이터가 없습니다."); @@ -64,7 +74,7 @@ const PumpingStationRvw = () => { console.error("중계펌프장 조회 중 에러 발생:", error); alert('데이터 조회 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + setIsLoading(false); } }; @@ -74,9 +84,9 @@ const PumpingStationRvw = () => { const returnFunc = () => console.log('returnFunc'); return ( -
- - +
+ +
diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index b42f1637a..9f40941f3 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -4,183 +4,193 @@ import type { GridView } from "@/components/type/DataGrid.type"; export const rawPumpingStationInfoList = { columns: [ - /* 1. 행정구역 및 기본정보 */ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' }, mergeCells: true }, // bscFctNm -> swtrmtFcltNm + { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, // pumpCls -> pumpSeNm + { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, // addr -> impdFcltPstnCn + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, // rnDt -> oprtngStrtYmd { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - } - ] - }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'pumpNm', fieldName: 'pumpNm', header: { text: '펌프장명' }, mergeCells: true, sortdisable: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - } - ] - }, - { name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '처리장명' } }, - { name: 'pumpCls', fieldName: 'pumpCls', header: { text: '구분\n(합류/분류)' } }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' } }, - { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동년월일\nyyyy-mm-dd' } }, - { - name: 'planDrhgAr', fieldName: 'planDrhgAr', header: { text: '계획배수면적\n(km2)' }, + name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' }, // planDrhgAr -> planMltvArea footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, /* 2. 계획배수량 (m3/hr) */ - { name: 'finewtDrngVl', fieldName: 'finewtDrngVl', header: { text: '청천시(A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtDrngVl', fieldName: 'rainwtDrngVl', header: { text: '우천시(B)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'vlWtr', fieldName: 'vlWtr', header: { text: '우수량(B-A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sndyArea', fieldName: 'sndyArea', header: { text: '청천시(A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // finewtDrngVl -> sndyArea + { name: 'rainArea', fieldName: 'rainArea', header: { text: '우천시(B)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtDrngVl -> rainArea + { name: 'exlnQnt', fieldName: 'exlnQnt', header: { text: '우수량(B-A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // vlWtr -> exlnQnt /* 3. 총 양수량 (m3/min) */ - { name: 'pltwtrTotCpcty', fieldName: 'pltwtrTotCpcty', header: { text: '오수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrTotCpcty', fieldName: 'rainwtrTotCpcty', header: { text: '우수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drtwPumpGrssPpqty', fieldName: 'drtwPumpGrssPpqty', header: { text: '오수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrTotCpcty -> drtwPumpGrssPpqty + { name: 'exlnPumpGrssPpqty', fieldName: 'exlnPumpGrssPpqty', header: { text: '우수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrTotCpcty -> exlnPumpGrssPpqty /* 4. 오수펌프 상세 (1, 2, 3) */ - { name: 'pltwtrPumpCalbr1', fieldName: 'pltwtrPumpCalbr1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCpcty1', fieldName: 'pltwtrPumpCpcty1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCnt1', fieldName: 'pltwtrPumpCnt1', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrPumpPreparCnt1', fieldName: 'pltwtrPumpPreparCnt1', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwPumpN1DmtrArea', fieldName: 'drtwPumpN1DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrPumpCalbr1 -> drtwPumpN1DmtrArea + { name: 'drtwPumpN1Ppqty', fieldName: 'drtwPumpN1Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrPumpCpcty1 -> drtwPumpN1Ppqty + { name: 'drtwPumpN1Cnt', fieldName: 'drtwPumpN1Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // pltwtrPumpCnt1 -> drtwPumpN1Cnt + { name: 'drtwPumpN1RsrvCnt', fieldName: 'drtwPumpN1RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // pltwtrPumpPreparCnt1 -> drtwPumpN1RsrvCnt - { name: 'pltwtrPumpCalbr2', fieldName: 'pltwtrPumpCalbr2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCpcty2', fieldName: 'pltwtrPumpCpcty2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCnt2', fieldName: 'pltwtrPumpCnt2', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrPumpPreparCnt2', fieldName: 'pltwtrPumpPreparCnt2', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwPumpN2DmtrArea', fieldName: 'drtwPumpN2DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drtwPumpN2Ppqty', fieldName: 'drtwPumpN2Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drtwPumpN2Cnt', fieldName: 'drtwPumpN2Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwPumpN2RsrvCnt', fieldName: 'drtwPumpN2RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrPumpCalbr3', fieldName: 'pltwtrPumpCalbr3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCpcty3', fieldName: 'pltwtrPumpCpcty3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'pltwtrPumpCnt3', fieldName: 'pltwtrPumpCnt3', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrPumpPreparCnt3', fieldName: 'pltwtrPumpPreparCnt3', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwPumpN3DmtrArea', fieldName: 'drtwPumpN3DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drtwPumpN3Ppqty', fieldName: 'drtwPumpN3Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drtwPumpN3Cnt', fieldName: 'drtwPumpN3Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwPumpN3RsrvCnt', fieldName: 'drtwPumpN3RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 5. 우수펌프 상세 (1, 2, 3) */ - { name: 'rainwtrPumpCalbr1', fieldName: 'rainwtrPumpCalbr1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCpcty1', fieldName: 'rainwtrPumpCpcty1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCnt1', fieldName: 'rainwtrPumpCnt1', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrPumpPreparCnt1', fieldName: 'rainwtrPumpPreparCnt1', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 5. 우수펌프 상세 (1, 2, 3) - N1, N2, N3*/ + { name: 'exlnPumpN1DmtrArea', fieldName: 'exlnPumpN1DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrPumpCalbr1 -> exlnPumpN1DmtrArea + { name: 'exlnPumpN1Ppqty', fieldName: 'exlnPumpN1Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrPumpCpcty1 -> exlnPumpN1Ppqty + { name: 'exlnPumpN1Cnt', fieldName: 'exlnPumpN1Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // rainwtrPumpCnt1 -> exlnPumpN1Cnt + { name: 'exlnPumpN4RsrvCnt', fieldName: 'exlnPumpN4RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // rainwtrPumpPreparCnt1 -> exlnPumpN4RsrvCnt + + { name: 'exlnPumpN2DmtrArea', fieldName: 'exlnPumpN2DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'exlnPumpN2Ppqty', fieldName: 'exlnPumpN2Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'exlnPumpN2Cnt', fieldName: 'exlnPumpN2Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnPumpN2RsrvCnt', fieldName: 'exlnPumpN2RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrPumpCalbr2', fieldName: 'rainwtrPumpCalbr2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCpcty2', fieldName: 'rainwtrPumpCpcty2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCnt2', fieldName: 'rainwtrPumpCnt2', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrPumpPreparCnt2', fieldName: 'rainwtrPumpPreparCnt2', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - { name: 'rainwtrPumpCalbr3', fieldName: 'rainwtrPumpCalbr3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCpcty3', fieldName: 'rainwtrPumpCpcty3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rainwtrPumpCnt3', fieldName: 'rainwtrPumpCnt3', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrPumpPreparCnt3', fieldName: 'rainwtrPumpPreparCnt3', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnPumpN3DmtrArea', fieldName: 'exlnPumpN3DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'exlnPumpN3Ppqty', fieldName: 'exlnPumpN3Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'exlnPumpN3Cnt', fieldName: 'exlnPumpN3Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnPumpN3RsrvCnt', fieldName: 'exlnPumpN3RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, /* 6. 기타 및 운영 */ - { name: 'elcpwAmnt', fieldName: 'elcpwAmnt', header: { text: '연간전력사용량\n(kWh/년)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + { name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '연간전력사용량\n(kWh/년)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // elcpwAmnt -> anlElpwrUseQnt + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, ], columnLayout: [ { name: 'groupA', header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, - 'gugun', - 'pumpNm', + { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + 'sggNm', + 'pumpstNm', ] }, 'displayYear', - 'bscFctNm', - 'pumpCls', - 'addr', - 'rnDt', - 'planDrhgAr', + 'swtrmtFcltNm', + 'pumpSeNm', + 'impdFcltPstnCn', + 'oprtngStrtYmd', + 'planMltvArea', { name: 'groupB', header: { text: '계획시간대최대배수량(m3/hr)' }, - items: ['finewtDrngVl', 'rainwtDrngVl', 'vlWtr'], + items: ['sndyArea', 'rainArea', 'exlnQnt'], }, { name: 'groupC', header: { text: '배수능력' }, items: [ - 'pltwtrTotCpcty', + 'drtwPumpGrssPpqty', { name: 'pumpO_Detail', header: { text: '오수펌프 상세' }, items: [ - { name: 'O1', header: { text: '오수펌프(1)' }, items: ['pltwtrPumpCalbr1', 'pltwtrPumpCpcty1', 'pltwtrPumpCnt1', 'pltwtrPumpPreparCnt1'] }, - { name: 'O2', header: { text: '오수펌프(2)' }, items: ['pltwtrPumpCalbr2', 'pltwtrPumpCpcty2', 'pltwtrPumpCnt2', 'pltwtrPumpPreparCnt2'] }, - { name: 'O3', header: { text: '오수펌프(3)' }, items: ['pltwtrPumpCalbr3', 'pltwtrPumpCpcty3', 'pltwtrPumpCnt3', 'pltwtrPumpPreparCnt3'] } + { name: 'O1', header: { text: '오수펌프(1)' }, items: ['drtwPumpN1DmtrArea', 'drtwPumpN1Ppqty', 'drtwPumpN1Cnt', 'drtwPumpN1RsrvCnt'] }, + { name: 'O2', header: { text: '오수펌프(2)' }, items: ['drtwPumpN2DmtrArea', 'drtwPumpN2Ppqty', 'drtwPumpN2Cnt', 'drtwPumpN2RsrvCnt'] }, + { name: 'O3', header: { text: '오수펌프(3)' }, items: ['drtwPumpN3DmtrArea', 'drtwPumpN3Ppqty', 'drtwPumpN3Cnt', 'drtwPumpN3RsrvCnt'] } ] }, - 'rainwtrTotCpcty', + 'exlnPumpGrssPpqty', { name: 'pumpW_Detail', header: { text: '우수펌프 상세' }, items: [ - { name: 'W1', header: { text: '우수펌프(1)' }, items: ['rainwtrPumpCalbr1', 'rainwtrPumpCpcty1', 'rainwtrPumpCnt1', 'rainwtrPumpPreparCnt1'] }, - { name: 'W2', header: { text: '우수펌프(2)' }, items: ['rainwtrPumpCalbr2', 'rainwtrPumpCpcty2', 'rainwtrPumpCnt2', 'rainwtrPumpPreparCnt2'] }, - { name: 'W3', header: { text: '우수펌프(3)' }, items: ['rainwtrPumpCalbr3', 'rainwtrPumpCpcty3', 'rainwtrPumpCnt3', 'rainwtrPumpPreparCnt3'] } + { name: 'W1', header: { text: '우수펌프(1)' }, items: ['exlnPumpN1DmtrArea', 'exlnPumpN1Ppqty', 'exlnPumpN1Cnt', 'exlnPumpN4RsrvCnt'] }, + { name: 'W2', header: { text: '우수펌프(2)' }, items: ['exlnPumpN2DmtrArea', 'exlnPumpN2Ppqty', 'exlnPumpN2Cnt', 'exlnPumpN2RsrvCnt'] }, + { name: 'W3', header: { text: '우수펌프(3)' }, items: ['exlnPumpN3DmtrArea', 'exlnPumpN3Ppqty', 'exlnPumpN3Cnt', 'exlnPumpN3RsrvCnt'] } ] } ], }, - 'elcpwAmnt', - 'rmk', + 'anlElpwrUseQnt', + 'rmrk', ], fixedColumns: 2, rowStartNum: 3 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 1e2a6336d..99f7ea552 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -92,12 +92,7 @@ export class StatisticsManagementApiService { * @param params (sido, gugun, yr, bsnmCd) */ public async getSewagePipeTypeList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - } + params: SewagePipeReviewRequestParams ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/type-status`, @@ -111,12 +106,7 @@ export class StatisticsManagementApiService { * @param params (sido, gugun, yr, bsnmCd) */ public async getSewagePipeDiamList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - } + params: SewagePipeReviewRequestParams ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/diam-status`, @@ -144,12 +134,7 @@ export class StatisticsManagementApiService { * @param params (yr, bsnmCd, sido, gugun) */ public async getSewerPipeDrdgList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - } + params: SewerPipeMtcRequestParams ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/dredging`, @@ -163,12 +148,7 @@ export class StatisticsManagementApiService { * @param params (yr, bsnmCd, sido, gugun) */ public async getSewerReservoirList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - } + params: SewerPipeMtcRequestParams ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/facilities`, @@ -182,12 +162,7 @@ export class StatisticsManagementApiService { * @param params (yr, bsnmCd, sido, gugun) */ public async getPumpStationList( - params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - } + params: SewerPipeMtcRequestParams ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/sewer-reservoir/pumping-stations`, @@ -202,11 +177,11 @@ export class StatisticsManagementApiService { */ public async getMediationPumpStationList( params: { - yr?: string; // 조회 연도 - bsnmCd?: string; // 사업소 코드 - sido?: string; // 시도 - gugun?: string; // 구군 - bscFctNm?: string; // 펌프장명 (시설명) + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + pumpstNm?: string; } ): Promise { const response = await stClient.get( diff --git a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts index fbfc04711..0f34edf86 100644 --- a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts @@ -1,67 +1,65 @@ import type { ApiResponse } from "./statistics-interface"; /** - * 중계펌프장 통계관리 아이템 인터페이스 + * 05.중계펌프장 검토 아이템 인터페이스 */ export interface MediationPumpStationItem { - // 기본 정보 및 행 제어 - sido: string; // 시도 - gugun: string; // 시군구 - adstCd: string; // 행정코드 - pumpNm: string; // 펌프장명 - yr: string; // 연도 - displayYear: string; // 구분 표시용 (2024년확정 등) - rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 + // 기본 정보 + ctpvNm: string; + sggNm: string; + admdstCd: string; + statsYr: string; + displayYear: string; + rowType: 'OLD' | 'CURR'; + pumpstNm: string; + impdFcltPstnCn: string; + oprtngStrtYmd: string; + pumpSeNm: string; + swtrmtFcltNm: string; + + // 제원 및 계획량 + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + + // 오수펌프 (DRTW) + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; - // 주요 제원 - bscFctNm: string; // 처리장명 - pumpCls: string; // 구분 (합류식/분류식) - addr: string; // 위치 - rnDt: string; // 가동년월일 - planDrhgAr: number; // 계획배수면적(㎢) - finewtDrngVl: number; // 계획시간대최대배수량 - 청천시(A) - rainwtDrngVl: number; // 계획시간대최대배수량 - 우천시(B) - vlWtr: number; // 계획시간대최대배수량 - 우수량(B-A) - - // 오수펌프 상세 (PLTWTR) - pltwtrTotCpcty: number; // 오수 총양수량(㎥/min) - pltwtrPumpCalbr1: number; // 오수펌프1 구경(mm) - pltwtrPumpCpcty1: number; // 오수펌프1 양수량(㎥/min) - pltwtrPumpCnt1: number; // 오수펌프1 펌프대수 - pltwtrPumpPreparCnt1: number; // 오수펌프1 예비대수 - - pltwtrPumpCalbr2: number; // 오수펌프2 구경(mm) - pltwtrPumpCpcty2: number; // 오수펌프2 양수량(㎥/min) - pltwtrPumpCnt2: number; // 오수펌프2 펌프대수 - pltwtrPumpPreparCnt2: number; // 오수펌프2 예비대수 - - pltwtrPumpCalbr3: number; // 오수펌프3 구경(mm) - pltwtrPumpCpcty3: number; // 오수펌프3 양수량(㎥/min) - pltwtrPumpCnt3: number; // 오수펌프3 펌프대수 - pltwtrPumpPreparCnt3: number; // 오수펌프3 예비대수 + drtwPumpN2DmtrArea: number; + drtwPumpN2Ppqty: number; + drtwPumpN2Cnt: number; + drtwPumpN2RsrvCnt: number; - // 우수펌프 상세 (RAINWTR) - rainwtrTotCpcty: number; // 우수 총양수량(㎥/min) - rainwtrPumpCalbr1: number; // 우수펌프1 구경(mm) - rainwtrPumpCpcty1: number; // 우수펌프1 양수량(㎥/min) - rainwtrPumpCnt1: number; // 우수펌프1 펌프대수 - rainwtrPumpPreparCnt1: number; // 우수펌프1 예비대수 - - rainwtrPumpCalbr2: number; // 우수펌프2 구경(mm) - rainwtrPumpCpcty2: number; // 우수펌프2 양수량(㎥/min) - rainwtrPumpCnt2: number; // 우수펌프2 펌프대수 - rainwtrPumpPreparCnt2: number; // 우수펌프2 예비대수 - - rainwtrPumpCalbr3: number; // 우수펌프3 구경(mm) - rainwtrPumpCpcty3: number; // 우수펌프3 양수량(㎥/min) - rainwtrPumpCnt3: number; // 우수펌프3 펌프대수 - rainwtrPumpPreparCnt3: number; // 우수펌프3 예비대수 + drtwPumpN3DmtrArea: number; + drtwPumpN3Ppqty: number; + drtwPumpN3Cnt: number; + drtwPumpN3RsrvCnt: number; + + // 우수펌프 (EXLN) + exlnPumpGrssPpqty: number; + exlnPumpN1DmtrArea: number; + exlnPumpN1Ppqty: number; + exlnPumpN1Cnt: number; + exlnPumpN4RsrvCnt: number; + + exlnPumpN2DmtrArea: number; + exlnPumpN2Ppqty: number; + exlnPumpN2Cnt: number; + exlnPumpN2RsrvCnt: number; + + exlnPumpN3DmtrArea: number; + exlnPumpN3Ppqty: number; + exlnPumpN3Cnt: number; + exlnPumpN3RsrvCnt: number; // 기타 - elcpwAmnt: number; // 연간전력사용량(kWh/년) - rmk: string; // 비고 - lcnt: string; // 당해 존재 여부 ('1'|'0') - ocnt: string; // 전년 존재 여부 ('1'|'0') + anlElpwrUseQnt: number; + rmrk: string; } @@ -77,10 +75,10 @@ export type MediationPumpStationApiResponse = ApiResponse; } \ No newline at end of file From d8c908b68e7f6183b92294b93858ba836e12f888 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 10:56:20 +0900 Subject: [PATCH 038/113] =?UTF-8?q?refactor:=2004.=ED=95=98=EC=88=98?= =?UTF-8?q?=EC=A0=80=EB=A5=98=EC=8B=9C=EC=84=A4=20=EB=B0=8F=20=EB=B9=97?= =?UTF-8?q?=EB=AC=BC=ED=8E=8C=ED=94=84=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/SewageReservoirPumpRvw.tsx | 42 +- .../review/SewageReservoirPumpRvwColumn.ts | 421 +++++++++--------- .../sewage-reservoir-pump-review-interface.ts | 235 +++++----- 3 files changed, 348 insertions(+), 350 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index 13303f6c1..4a250d2aa 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -13,11 +13,12 @@ import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/ import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface"; /** * 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장 - * Tab A: 하수저류시설 - * Tab B: 빗물펌프장 + * Tab A: 하수저류시설 (ST00040001) + * Tab B: 빗물펌프장 (ST00040002) */ const SewageReservoirPumpRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -25,7 +26,7 @@ const SewageReservoirPumpRvw = () => { const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState('ST00040001'); - const searchParamRef = useRef(null); + const searchParamRef = useRef(null); const columnInfo = useMemo(() => { return SewageReservoirPumpInfoListRvw.map((tab: ColumnInfoList) => ({ @@ -56,16 +57,15 @@ const SewageReservoirPumpRvw = () => { try { setIsLoading(true); - const searchParam = { - sido: values.sido, - gugun: values.gugun === 'ALL' ? '' : values.gugun, - bsnmCd: values.bsnmCd, - yr: values.year, + const searchParam: SewerReservoirRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, }; searchParamRef.current = searchParam; - - //하수저류시설 (Tab A) + const response = await statisticsManagementApi.getSewerReservoirList(searchParam); if (response?.status === 'success' && response.data) { @@ -85,15 +85,21 @@ const SewageReservoirPumpRvw = () => { const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); + const existingAData = rowDataMap.get('ST00040001'); const activeTabData = rowDataMap.get(tabKey); + + const hasAData = Array.isArray(existingAData) && existingAData.length > 0; const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; - if (searchParamRef.current && !hasActiveTabData) { + if (hasAData && !hasActiveTabData && searchParamRef.current) { try { setIsLoading(true); + let response; - if (tabKey === 'ST00040002') { + if (tabKey === 'ST00040001') { + response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current); + } else if (tabKey === 'ST00040002') { response = await statisticsManagementApi.getPumpStationList(searchParamRef.current); } @@ -116,19 +122,19 @@ const SewageReservoirPumpRvw = () => { return (
- +
- { /* 검토의견 로직 */ }} /> - { /* 승인 로직 */ }} /> - { /* 반려 로직 */ }} /> - { /* 저장 로직 */ }} /> + { }} /> + { }} /> + { }} /> + { }} />
{isLoading && ( -
+
diff --git a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts index 83d0ab65a..d6bc43849 100644 --- a/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageReservoirPumpRvwColumn.ts @@ -1,5 +1,4 @@ import { ValueType } from "realgrid"; - import { footerIntArrayByDivision, footerDecimal1ArrayByDivision, @@ -13,12 +12,12 @@ const rawReservoirList = [ tabKey: 'ST00040001', tabLabel: '하수저류시설현황', rowStartNum: 3, - fixedColumns: 2, + fixedColumns: 4, columns: [ /* 1. 기본 정보 */ { - name: 'sido', - fieldName: 'sido', + name: 'ctpvNm', + fieldName: 'ctpvNm', header: { text: '시도' }, mergeCells: true, sortdisable: true, @@ -36,8 +35,7 @@ const rawReservoirList = [ } } const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; } }, { @@ -53,15 +51,15 @@ const rawReservoirList = [ } } const divisionValues = Array.from(uniqueValues); - if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계'; - return '합계'; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; } } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true}, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + { name: 'impdFcltNm', fieldName: 'impdFcltNm', header: { text: '저류시설명' }, mergeCells: true, sortdisable: true }, + { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '저류시설위치' }, mergeCells: true, sortdisable: true }, { name: 'displayYear', fieldName: 'displayYear', @@ -81,7 +79,7 @@ const rawReservoirList = [ } } const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : ''; } }, { @@ -97,82 +95,84 @@ const rawReservoirList = [ } } const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : ''; } } ] }, - /* 2. 시설 제원 */ - { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동개시 년월일' } }, - { name: 'plDsgnFrqnc', fieldName: 'plDsgnFrqnc', header: { text: '계획설계빈도' } }, - { name: 'insPer1', fieldName: 'insPer1', header: { text: '침수예방' } }, - { name: 'insPer2', fieldName: 'insPer2', header: { text: '수질오염저감' } }, - { name: 'insPer3', fieldName: 'insPer3', header: { text: '하수 재이용 용도' } }, - { name: 'facRes', fieldName: 'facRes', header: { text: '시설구조' } }, - { name: 'dgrAr', fieldName: 'dgrAr', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, - { name: 'swgeSgbsCpcty', fieldName: 'swgeSgbsCpcty', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + /* 2. 시설 제원 */ + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시 년월일' } }, + { name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } }, + { name: 'fldngPrvtnYn', fieldName: 'fldngPrvtnYn', header: { text: '침수예방' } }, + { name: 'wqPlltnMtgtYn', fieldName: 'wqPlltnMtgtYn', header: { text: '수질오염저감' } }, + { name: 'rtlztnUsgYn', fieldName: 'rtlztnUsgYn', header: { text: '하수 재이용 용도' } }, + { name: 'fcltStrctCn', fieldName: 'fcltStrctCn', header: { text: '시설구조' } }, + { name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, + { name: 'impdFcltCpct', fieldName: 'impdFcltCpct', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, /* 3. 펌프시설 */ - { name: 'totCpcty', fieldName: 'totCpcty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCalbr', fieldName: 'pumpCalbr', header: { text: '구경(mm)' } }, - { name: 'wpCpcty', fieldName: 'wpCpcty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo', fieldName: 'pumpCo', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo', fieldName: 'pumpPreparCo', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpCalbr2', fieldName: 'pumpCalbr2', header: { text: '구경(mm)' } }, - { name: 'wpCpcty2', fieldName: 'wpCpcty2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo2', fieldName: 'pumpCo2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo2', fieldName: 'pumpPreparCo2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpCalbr3', fieldName: 'pumpCalbr3', header: { text: '구경(mm)' } }, - { name: 'wpCpcty3', fieldName: 'wpCpcty3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo3', fieldName: 'pumpCo3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo3', fieldName: 'pumpPreparCo3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + { name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + { name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, /* 4. 부대시설 및 방식 */ - { name: 'freFnnc', fieldName: 'freFnnc', header: { text: '전처리시설(유/무)' } }, - { name: 'dorzeqpFnnc', fieldName: 'dorzeqpFnnc', header: { text: '탈취설비(유/무)' } }, - { name: 'selfEtrqFnnc', fieldName: 'selfEtrqFnnc', header: { text: '자가발전(유/무)' } }, - { name: 'draMth', fieldName: 'draMth', header: { text: '배수방식' } }, - { name: 'elecMth', fieldName: 'elecMth', header: { text: '저류방식' } }, - { name: 'elecProMat', fieldName: 'elecProMat', header: { text: '저류수 처리방법' } }, - { name: 'disWat', fieldName: 'disWat', header: { text: '방류수역' } }, + { name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설(유/무)' } }, + { name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비(유/무)' } }, + { name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전(유/무)' } }, + { name: 'mltvMthCn', fieldName: 'mltvMthCn', header: { text: '배수방식' } }, + { name: 'impdMthCn', fieldName: 'impdMthCn', header: { text: '저류방식' } }, + { name: 'impdPrcsMthdCn', fieldName: 'impdPrcsMthdCn', header: { text: '저류수 처리방법' } }, + { name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역' } }, /* 5. 최종처분량 및 방법 */ - { name: 'sdmtTreCpcty', fieldName: 'sdmtTreCpcty', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'iprtTreCpcty', fieldName: 'iprtTreCpcty', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'sdmtTreMth', fieldName: 'sdmtTreMth', header: { text: '침사' } }, - { name: 'iprtTreMth', fieldName: 'iprtTreMth', header: { text: '협잡물' } }, + { name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'prcptnLastDspsMthdExpln', fieldName: 'prcptnLastDspsMthdExpln', header: { text: '침사' } }, + { name: 'ctmnLastDspsMthdExpln', fieldName: 'ctmnLastDspsMthdExpln', header: { text: '협잡물' } }, /* 6. 운영실적 */ - { name: 'yrstpRnDyn', fieldName: 'yrstpRnDyn', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'dsplcmntAmnt', fieldName: 'dsplcmntAmnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + { name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, ], columnLayout: [ { name: 'areaGroup', header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'gugun', 'swgeStrgeNm', 'addr' + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm','admdstNm' ] }, - 'displayYear', 'rnDt', 'plDsgnFrqnc', - { name: 'purposeGroup', header: { text: '설치목적' }, items: ['insPer1', 'insPer2', 'insPer3'] }, - 'facRes', 'dgrAr', 'swgeSgbsCpcty', + 'impdFcltNm', 'impdFcltPstnCn','displayYear', 'oprtngStrtYmd', 'planDesignFreqCnt', + { name: 'purposeGroup', header: { text: '설치목적' }, items: ['fldngPrvtnYn', 'wqPlltnMtgtYn', 'rtlztnUsgYn'] }, + 'fcltStrctCn', 'wtrStrgZoneArea', 'impdFcltCpct', { name: 'pumpMainGroup', header: { text: '펌프시설' }, items: [ - 'totCpcty', - { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpCalbr', 'wpCpcty', 'pumpCo', 'pumpPreparCo'] }, - { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpCalbr2', 'wpCpcty2', 'pumpCo2', 'pumpPreparCo2'] }, - { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpCalbr3', 'wpCpcty3', 'pumpCo3', 'pumpPreparCo3'] } + 'grssPpqty', + { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpFcltN1DmtrExpln', 'pumpFcltN1PerPpqty', 'pumpFcltN1PumpCnt', 'pumpFcltN1RsrvPumpCnt'] }, + { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpFcltN2DmtrExpln', 'pumpFcltN2PerPpqty', 'pumpFcltN2PumpCnt', 'pumpFcltN2RsrvPumpCnt'] }, + { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpFcltN3DmtrExpln', 'pumpFcltN3PerPpqty', 'pumpFcltN3PumpCnt', 'pumpFcltN3RsrvPumpCnt'] } ] }, - { name: 'subFacGroup', header: { text: '부대시설' }, items: ['freFnnc', 'dorzeqpFnnc', 'selfEtrqFnnc'] }, - 'draMth', 'elecMth', 'elecProMat', 'disWat', - { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['sdmtTreCpcty', 'iprtTreCpcty'] }, - { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['sdmtTreMth', 'iprtTreMth'] }, - { name: 'opGroup', header: { text: '운영실적' }, items: ['yrstpRnDyn', 'dsplcmntAmnt'] }, - 'rmk' + { name: 'subFacGroup', header: { text: '부대시설' }, items: ['prcsgFcltYn', 'ddrztnEqpmYn', 'oslfEtgtYn'] }, + 'mltvMthCn', 'impdMthCn', 'impdPrcsMthdCn', 'dschgWtrsExpln', + { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['prcptnLastDspsQnt', 'ctmnLastDspsQnt'] }, + { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['prcptnLastDspsMthdExpln', 'ctmnLastDspsMthdExpln'] }, + { name: 'opGroup', header: { text: '운영실적' }, items: ['oprtngDayCnt', 'grssMltvQnt'] }, + 'rmrk' ] }, { @@ -180,155 +180,162 @@ const rawReservoirList = [ tabLabel: '빗물펌프장', rowStartNum: 3, fixedColumns: 2, - columns: [ - /* 1. 기본 정보 */ - { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - 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)); - } + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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 '합계'; - } - }, - { - valueCallback: (grid: GridView) => { - 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, sortdisable: true }, - { name: 'swgeStrgeNm', fieldName: 'swgeStrgeNm', header: { text: '시설명' }, mergeCells: true }, - { name: 'addr', fieldName: 'addr', header: { text: '위치' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - 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)); - } + }, + { + valueCallback: (grid: GridView) => { + 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] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - 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); + if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + ' 합계'; + return '합계'; } - ] - }, - { name: 'fctKnd', fieldName: 'fctKnd', header: { text: '시설물종류' } }, - { name: 'rnDt', fieldName: 'rnDt', header: { text: '가동개시 년월일' } }, - { name: 'plDsgnFrqnc', fieldName: 'plDsgnFrqnc', header: { text: '계획설계빈도' } }, - { name: 'dgrAr', fieldName: 'dgrAr', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, - { name: 'revFnnc', fieldName: 'revFnnc', header: { text: '유수지유무\n(유,무)' } }, - { name: 'revCpcty', fieldName: 'revCpcty', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'absCpcty', fieldName: 'absCpcty', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'swgeSgbsCpcty', fieldName: 'swgeSgbsCpcty', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'totCpcty', fieldName: 'totCpcty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCalbr', fieldName: 'pumpCalbr', header: { text: '구경(mm)' } }, - { name: 'wpCpcty', fieldName: 'wpCpcty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo', fieldName: 'pumpCo', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo', fieldName: 'pumpPreparCo', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpCalbr2', fieldName: 'pumpCalbr2', header: { text: '구경(mm)' } }, - { name: 'wpCpcty2', fieldName: 'wpCpcty2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo2', fieldName: 'pumpCo2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo2', fieldName: 'pumpPreparCo2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpCalbr3', fieldName: 'pumpCalbr3', header: { text: '구경(mm)' } }, - { name: 'wpCpcty3', fieldName: 'wpCpcty3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, - { name: 'pumpCo3', fieldName: 'pumpCo3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'pumpPreparCo3', fieldName: 'pumpPreparCo3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'freFnnc', fieldName: 'freFnnc', header: { text: '전처리시설' } }, - { name: 'dorzeqpFnnc', fieldName: 'dorzeqpFnnc', header: { text: '탈취설비' } }, - { name: 'selfEtrqFnnc', fieldName: 'selfEtrqFnnc', header: { text: '자가발전' } }, - { name: 'disWat', fieldName: 'disWat', header: { text: '방류수역' } }, - { name: 'sdmtTreCpcty', fieldName: 'sdmtTreCpcty', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'iprtTreCpcty', fieldName: 'iprtTreCpcty', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'sdmtTreMth', fieldName: 'sdmtTreMth', header: { text: '침사(방법)' } }, - { name: 'iprtTreMth', fieldName: 'iprtTreMth', header: { text: '협잡물(방법)' } }, - { name: 'yrstpRnDyn', fieldName: 'yrstpRnDyn', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'dsplcmntAmnt', fieldName: 'dsplcmntAmnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'areaGroup', header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'gugun', 'swgeStrgeNm', 'addr' - ] - }, - 'displayYear', 'fctKnd', 'rnDt', 'plDsgnFrqnc', 'dgrAr', - { name: 'revGroup', header: { text: '유수지' }, items: ['revFnnc', 'revCpcty'] }, - { name: 'absGroup', header: { text: '흡수정' }, items: ['absCpcty'] }, - 'swgeSgbsCpcty', - { - name: 'pumpMainGroup', header: { text: '펌프시설' }, - items: [ - 'totCpcty', - { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpCalbr', 'wpCpcty', 'pumpCo', 'pumpPreparCo'] }, - { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpCalbr2', 'wpCpcty2', 'pumpCo2', 'pumpPreparCo2'] }, - { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpCalbr3', 'wpCpcty3', 'pumpCo3', 'pumpPreparCo3'] } - ] - }, - { name: 'subFacGroup', header: { text: '부대시설' }, items: ['freFnnc', 'dorzeqpFnnc', 'selfEtrqFnnc'] }, - 'disWat', - { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['sdmtTreCpcty', 'iprtTreCpcty'] }, - { name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['sdmtTreMth', 'iprtTreMth'] }, - { name: 'opGroup', header: { text: '운영실적' }, items: ['yrstpRnDyn', 'dsplcmntAmnt'] }, - 'rmk' - ] - } + } + ] + }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + { name: 'impdFcltNm', fieldName: 'impdFcltNm', header: { text: '시설명' }, mergeCells: true, sortdisable: true }, + { name: 'impdFcltPstnCn',fieldName: 'impdFcltPstnCn',header: { text: '위치' }, mergeCells: true, sortdisable: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) => { + 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. 시설 제원 */ + { name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } }, + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시 년월일' } }, + { name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } }, + { name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision }, + { name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무\n(유,무)' } }, + { name: 'rsvrImpndQnt', fieldName: 'rsvrImpndQnt', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'abstnRgnImdQnt', fieldName: 'abstnRgnImdQnt', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + + /* 3. 펌프시설 */ + { name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + { name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + { name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } }, + { name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision }, + { name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + /* 4. 부대시설 */ + { name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설' } }, + { name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비' } }, + { name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전' } }, + { name: 'dschgWtrsExpln',fieldName: 'dschgWtrsExpln',header: { text: '방류수역' } }, + + /* 5. 최종처분량 */ + { name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + + /* 6. 운영실적 */ + { name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', 'admdstNm', + ] + }, + 'displayYear', 'fcltyKndCn', 'oprtngStrtYmd', 'planDesignFreqCnt', 'wtrStrgZoneArea', + { name: 'revGroup', header: { text: '유수지' }, items: ['rsvrYn', 'rsvrImpndQnt'] }, + { name: 'absGroup', header: { text: '흡수정' }, items: ['abstnRgnImdQnt'] }, + 'fcltCpct', + { + name: 'pumpMainGroup', header: { text: '펌프시설' }, + items: [ + { name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpFcltN1DmtrExpln', 'pumpFcltN1PerPpqty', 'pumpFcltN1PumpCnt', 'pumpFcltN1RsrvPumpCnt'] }, + { name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpFcltN2DmtrExpln', 'pumpFcltN2PerPpqty', 'pumpFcltN2PumpCnt', 'pumpFcltN2RsrvPumpCnt'] }, + { name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpFcltN3DmtrExpln', 'pumpFcltN3PerPpqty', 'pumpFcltN3PumpCnt', 'pumpFcltN3RsrvPumpCnt'] } + ] + }, + { name: 'subFacGroup', header: { text: '부대시설' }, items: ['prcsgFcltYn', 'ddrztnEqpmYn', 'oslfEtgtYn'] }, + 'dschgWtrsExpln', + { name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['prcptnLastDspsQnt', 'ctmnLastDspsQnt'] }, + { name: 'opGroup', header: { text: '운영실적' }, items: ['oprtngDayCnt', 'grssMltvQnt'] }, + 'rmrk' + ] +} ]; export const SewageReservoirPumpInfoListRvw = rawReservoirList.map(tab => ({ diff --git a/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts b/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts index 4f7b6bf7f..d27d770f5 100644 --- a/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts +++ b/apps/st/src/entities/statistics/model/sewage-reservoir-pump-review-interface.ts @@ -5,143 +5,134 @@ import type { ApiResponse } from "./statistics-interface"; * 04. 하수저류시설 tab1. 시설현황 아이템 인터페이스 (Tab1) */ export interface SewerReservoirRegionalItem { -// 1. 기본 정보 및 행 제어 - sido: string; // 시도 - gugun: string; // 시군구 - adstCd: string; // 행정코드 - swgeStrgeNm: string; // 시설명 - addr: string; // 위치(주소) - yr: string; // 연도 - rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 - displayYear: string; // 구분 표시용 (2024년, 2023년확정 등) +// 1. 기본 정보 + statsYr: string; // 통계연도 + ctpvNm: string; // 시도명 + sggNm: string; // 시군구명 + admdstNm: string; // 행정구역명 + admdstCd: string; // 행정구역코드 + sewerBzmnCd: string; // 하수도사업자코드 + displayYear: string; // 화면 표시 연도 + impdFcltNm: string; // 저류시설명 + impdFcltPstnCn: string; // 저류시설위치내용 + rowType: 'OLD' | 'CURR'; // 데이터 구분 // 2. 시설 제원 - rnDt: string; // 가동개시일 (yyyyMMdd) - plDsgnFrqnc: string; // 계획설계빈도 - insPer1: string; // 설치목적: 침수예방 - insPer2: string; // 설치목적: 수질오염저감 - insPer3: string; // 설치목적: 하수재이용 - facRes: string; // 시설구조 - dgrAr: number; // 집수구역면적 - swgeSgbsCpcty: number; // 시설용량 - totCpcty: number; // 총양수량 + oprtngStrtYmd: string; // 가동개시일자 + planDesignFreqCnt: number; // 계획설계빈도 + fldngPrvtnYn: string; // 침수예방 + wqPlltnMtgtYn: string; // 수질오염저감 + rtlztnUsgYn: string; // 하수재이용용도 + fcltStrctCn: string; // 시설구조 + wtrStrgZoneArea: number; // 집수구역면적 + impdFcltCpct: number; // 시설용량 + grssPpqty: number; // 총양수량 - // 3. 펌프시설 상세 (1호기) - pumpCalbr: string; // 구경 - wpCpcty: number; // 대당 양수량 - pumpCo: number; // 펌프대수 - pumpPreparCo: number; // 예비펌프대수 + // 3. 펌프시설 상세 (1~3호기) + pumpFcltN1DmtrExpln: string; // 1구경 + pumpFcltN1PerPpqty: number; // 1대당양수량 + pumpFcltN1PumpCnt: number; // 1펌프대수 + pumpFcltN1RsrvPumpCnt: number; // 1예비펌프대수 - // 4. 펌프시설 상세 (2호기) - 추가 - pumpCalbr2: string; // 구경2 - wpCpcty2: number; // 대당 양수량2 - pumpCo2: number; // 펌프대수2 - pumpPreparCo2: number; // 예비펌프대수2 + pumpFcltN2DmtrExpln: string; // 2구경 + pumpFcltN2PerPpqty: number; // 2대당양수량 + pumpFcltN2PumpCnt: number; // 2펌프대수 + pumpFcltN2RsrvPumpCnt: number; // 2예비펌프대수 - // 5. 펌프시설 상세 (3호기) - 추가 - pumpCalbr3: string; // 구경3 - wpCpcty3: number; // 대당 양수량3 - pumpCo3: number; // 펌프대수3 - pumpPreparCo3: number; // 예비펌프대수3 + pumpFcltN3DmtrExpln: string; // 3구경 + pumpFcltN3PerPpqty: number; // 3대당양수량 + pumpFcltN3PumpCnt: number; // 3펌프대수 + pumpFcltN3RsrvPumpCnt: number; // 3예비펌프대수 - // 6. 부대시설 및 방식 - 추가 - freFnnc: string; // 전처리시설유무 - dorzeqpFnnc: string; // 탈취설비유무 - selfEtrqFnnc: string; // 자가발전유무 - draMth: string; // 배수방식 - elecMth: string; // 저류방식 - elecProMat: string; // 저류수 처리방법 - disWat: string; // 방류수역 + // 4. 부대시설 및 방식 + prcsgFcltYn: string; // 전처리시설 유무 + ddrztnEqpmYn: string; // 탈취설비 유무 + oslfEtgtYn: string; // 자가발전 유무 + mltvMthCn: string; // 배수방식 + impdMthCn: string; // 저류방식 + impdPrcsMthdCn: string; // 저류수 처리방법 + dschgWtrsExpln: string; // 방류수역 - // 7. 최종처분량 및 방법 - 추가 - sdmtTreCpcty: number; // 침사량 - iprtTreCpcty: number; // 협잡물량 - sdmtTreMth: string; // 침사 처분방법 - iprtTreMth: string; // 협잡물 처분방법 + // 5. 최종처분량 및 방법 + prcptnLastDspsQnt: number; // 침사 최종처분량 + ctmnLastDspsQnt: number; // 협잡물 최종처분량 + prcptnLastDspsMthdExpln: string; // 침사 최종처분방법 + ctmnLastDspsMthdExpln: string; // 협잡물 최종처분방법 - // 8. 운영 실적 - yrstpRnDyn: number; // 가동일수 - dsplcmntAmnt: number; // 총배수량 - rmk: string; // 비고 - - // 9. 상태 체크용 (그리드 배경색 제어용) - 추가 - lcnt: number; // 신규 여부 (1/0) - ocnt: number; // 기존 여부 (1/0) -} - -/** - * 하수저류시설 응답 데이터 구조 - */ -export interface SewerReservoirResponseData { - reservoirList: SewerReservoirRegionalItem[]; -} - -/** - * 하수저류시설 서비스 인터페이스 - */ -export interface SewerReservoirService { - /** - * 하수저류시설 리스트 조회 (Tab1) - */ - getSewerReservoirList(params: { - yr?: string; // 조회 연도 - bsnmCd?: string; // 사업장 코드 - sido?: string; // 시도 - gugun?: string; // 구군 - }): Promise; + // 6. 운영 실적 + oprtngDayCnt: number; // 가동일수 + grssMltvQnt: number; // 총배수량 + rmrk: string; // 비고 } /** * 04. 빗물펌프장 tab2. 시설현황 아이템 인터페이스 (Tab2) */ + export interface PumpingStationRegionalItem { // --- 기본 정보 --- - sido: string; // 시도 - gugun: string; // 시군구 - adstCd: string; // 행정코드 - swgeStrgeNm: string; // 시설명 - addr: string; // 위치 - yr: string; // 연도 - rowType: 'OLD' | 'CURR'; // 전년도/당해년도 구분 - displayYear: string; // 구분 표시용 + ctpvNm: string; // 시도명 + sggNm: string; // 시군구명 + admdstNm: string; // 행정구역명 + admdstCd: string; // 행정구역코드 + sewerBzmnCd: string; // 하수도사업자코드 + displayYear: string; // 화면 표시 연도 + impdFcltNm: string; // 저류시설명 + impdFcltPstnCn: string; // 저류시설위치내용 + rowType: 'OLD' | 'CURR'; // 데이터 구분 // --- 시설 제원 --- - fctKnd: string; // 시설물종류 - rnDt: string; // 가동개시일 - plDsgnFrqnc: string; // 계획설계빈도 - dgrAr: number; // 집수구역면적 - revFnnc: string; // 유수지유무 - revCpcty: number; // 유수지저류량 - absCpcty: number; // 흡수정저류량 - swgeSgbsCpcty: number; // 시설용량 - totCpcty: number; // 총양수량 + fcltyKndCn: string; // 시설물종류 + oprtngStrtYmd: string; // 가동개시일자 + planDesignFreqCnt: number; // 계획설계빈도 + wtrStrgZoneArea: number; // 집수구역면적 - // --- 펌프시설 상세 (1, 2, 3호기) --- - pumpCalbr: string; wpCpcty: number; pumpCo: number; pumpPreparCo: number; - pumpCalbr2: string; wpCpcty2: number; pumpCo2: number; pumpPreparCo2: number; - pumpCalbr3: string; wpCpcty3: number; pumpCo3: number; pumpPreparCo3: number; + // 유수지 + rsvrYn: string; // 유수지유무 + rsvrImpndQnt: number; // 저류량 + abstnRgnImdQnt: number; // 흡수지 저류량 - // --- 부대시설 및 방식 --- - freFnnc: string; // 전처리시설유무 - dorzeqpFnnc: string; // 탈취설비유무 - selfEtrqFnnc: string; // 자가발전유무 - disWat: string; // 방류수역 + fcltCpct: number; // 시설용량 - // --- 최종처분량 및 방법 --- - sdmtTreCpcty: number; // 침사 처분량 - iprtTreCpcty: number; // 협잡물 처분량 - sdmtTreMth: string; // 침사 처분방법 - iprtTreMth: string; // 협잡물 처분방법 + // --- 펌프시설 상세 (1~3호기) --- + pumpFcltN1DmtrExpln: string; // 펌프시설1 구경 + pumpFcltN1PerPpqty: number; // 펌프시설1 대당양수량 + pumpFcltN1PumpCnt: number; // 펌프시설1 펌프대수 + pumpFcltN1RsrvPumpCnt: number; // 펌프시설1 예비펌프대수 + + pumpFcltN2DmtrExpln: string; // 펌프시설2 구경 + pumpFcltN2PerPpqty: number; // 펌프시설2 대당양수량 + pumpFcltN2PumpCnt: number; // 펌프시설2 펌프대수 + pumpFcltN2RsrvPumpCnt: number; // 펌프시설2 예비펌프대수 + + pumpFcltN3DmtrExpln: string; // 펌프시설3 구경 + pumpFcltN3PerPpqty: number; // 펌프시설3 대당양수량 + pumpFcltN3PumpCnt: number; // 펌프시설3 펌프대수 + pumpFcltN3RsrvPumpCnt: number; // 펌프시설3 예비펌프대수 + + // --- 부대시설 --- + prcsgFcltYn: string; // 전처리시설 유무 + ddrztnEqpmYn: string; // 탈취설비 유무 + oslfEtgtYn: string; // 자가발전 유무 + dschgWtrsExpln: string; // 방류수역 + + // --- 최종처분량 --- + prcptnLastDspsQnt: number; // 침사 최종처분량 + ctmnLastDspsQnt: number; // 협잡물 최종처분량 // --- 운영실적 --- - yrstpRnDyn: number; // 가동일수 - dsplcmntAmnt: number; // 총배수량 - rmk: string; // 비고 + oprtngDayCnt: number; // 가동일수 + grssMltvQnt: number; // 총배수량 + rmrk: string; // 비고 +} + +export interface SewerReservoirRequestParams { + ctpvNm?: string; // 시도명 + sggNm?: string; // 시군구명 + statsYr?: string; // 통계 연도 + sewerBzmnCd?: string; // 하수도사업자 코드 } -/** - * 응답 타입 정의 - */ export type SewerReservoirRegionalApiResponse = ApiResponse; export type PumpingStationRegionalApiResponse = ApiResponse; @@ -152,20 +143,14 @@ export interface SewerReservoirService { /** * 하수저류시설 리스트 조회 (Tab1) */ - getSewerReservoirList(params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - }): Promise; + getSewerReservoirList( + params: SewerReservoirRequestParams + ): Promise; /** * 빗물펌프장 리스트 조회 (Tab2) */ - getPumpStationList(params: { - yr?: string; - bsnmCd?: string; - sido?: string; - gugun?: string; - }): Promise; + getPumpStationList( + params: SewerReservoirRequestParams + ): Promise; } \ No newline at end of file From 5f639a2fa98c47dbafd04ea847f79d7157b32914 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 10:58:06 +0900 Subject: [PATCH 039/113] =?UTF-8?q?refactor:=2003.=20=ED=95=98=EC=88=98?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=20=EC=9C=A0=EC=A7=80=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=91=9C=EC=A4=80=ED=99=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/SewagePipeMaintainRvw.tsx | 2 +- .../review/SewagePipeMaintainRvwColumn.ts | 161 +++++++++++------- ...istics-sewage-pipe-mtc-review-interface.ts | 6 +- 3 files changed, 101 insertions(+), 68 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index d8cdc7d0e..26629d3a5 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -101,7 +101,7 @@ const SewagePipeMaintainRvw = () => { if (tabKey === 'ST00030001') { response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current); } else if (tabKey === 'ST00030002') { - // response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current); + response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current); } if (response?.status === 'success' && response.data) { diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index cbc536253..de413a34b 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -93,14 +93,19 @@ const rawMaintainList = [ } ] }, + /* 전체보수 (m) */ { name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + /* 부분보수 (개소) */ { name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 부속시설 (개소) */ { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식\n맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, @@ -108,6 +113,8 @@ const rawMaintainList = [ { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, { name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 조사량 및 비고 */ { name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, ], @@ -116,7 +123,7 @@ const rawMaintainList = [ name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }, { colspan: 2 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', 'admdstNm', ] @@ -148,10 +155,9 @@ const rawMaintainList = [ rowStartNum: 4, fixedColumns: 2, columns: [ - /* 1. 행정 및 기본 정보 */ - { - name: 'sido', - fieldName: 'sido', + { + name: 'ctpvNm', + fieldName: 'ctpvNm', header: { text: '시도' }, mergeCells: true, sortdisable: true, @@ -164,7 +170,9 @@ const rawMaintainList = [ const uniqueValues = new Set(); for (let i = 0; i < rowCount; i++) { const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } const divisionValues = Array.from(uniqueValues); return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; @@ -178,7 +186,9 @@ const rawMaintainList = [ const uniqueValues = new Set(); for (let i = 0; i < rowCount; i++) { const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); + if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') { + uniqueValues.add(String(divisionValue)); + } } const divisionValues = Array.from(uniqueValues); return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; @@ -186,7 +196,8 @@ const rawMaintainList = [ } ] }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', @@ -195,83 +206,101 @@ const rawMaintainList = [ footer: [ { valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); const uniqueValues = new Set(); - for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { - const val = grid.getDataSource().getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + 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 > 0 ? divisionValues[0] + ' 합계' : ''; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : ''; } }, { valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); const uniqueValues = new Set(); - for (let i = 0; i < grid.getDataSource().getRowCount(); i++) { - const val = grid.getDataSource().getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + 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 > 1 ? divisionValues[1] + ' 합계' : ''; + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : ''; } } ] }, + /* 2. 시설 기본 정보 */ + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 2. 시설 기본 정보 */ - { name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 3. 준설연장 및 개소수 */ + { name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'stofcbrOtflCnt', fieldName: 'stofcbrOtflCnt', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 3. 준설연장 및 개소수 */ - { name: 'drdgPipeSm', fieldName: 'drdgPipeSm', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgCmbnPipe', fieldName: 'drdgCmbnPipe', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgRainwtrPipe', fieldName: 'drdgRainwtrPipe', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgPltwtrPipe', fieldName: 'drdgPltwtrPipe', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'soilspllSoilholeSite', fieldName: 'soilspllSoilholeSite', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 4. 준설 계획 및 율 */ + { name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '전체연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 4. 준설 계획 및 율 */ - { name: 'drdgTrgetExtn', fieldName: 'drdgTrgetExtn', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'drdgTrgetRt', fieldName: 'drdgTrgetRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'piplFcltyRt', fieldName: 'piplFcltyRt', header: { text: '준설계획\n연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + /* 5. 준설실적 */ + { name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drdgnQnt', fieldName: 'drdgnQnt', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - /* 5. 준설실적 */ - { name: 'drdgAcmsltAmount', fieldName: 'drdgAcmsltAmount', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drdgAcmsltQy', fieldName: 'drdgAcmsltQy', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + /* 6. 준설토사처리 */ + { name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설단가\n(원/m3)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양방출' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 6. 준설토사처리 */ - { name: 'drdgAcmsltUntpc', fieldName: 'drdgAcmsltUntpc', header: { text: '준설토\n처리비\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drdgEtndSm', fieldName: 'drdgEtndSm', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rclm', fieldName: 'rclm', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'oceanDpsl', fieldName: 'oceanDpsl', header: { text: '해양투기' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ruse', fieldName: 'ruse', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'etc', fieldName: 'etc', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 7. 기타 및 의견 */ - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: [{ column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'gugun'] }, - 'displayYear', 'piplFcltyExtn', - { - name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, - items: [ - { - name: 'pipeLenGroup', header: { text: '관로(m)' }, - items: ['drdgPipeSm', 'drdgCmbnPipe', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['drdgRainwtrPipe', 'drdgPltwtrPipe'] }] - }, - 'mnhleSite', 'pltwtrRcvrSite', 'soilspllSoilholeSite' - ] - }, - 'drdgTrgetExtn', 'drdgTrgetRt', 'piplFcltyRt', - { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgAcmsltAmount', 'drdgAcmsltQy'] }, - { - name: 'processGroup', header: { text: '준설토사처리' }, - items: ['drdgAcmsltUntpc', 'drdgEtndSm', 'rclm', 'oceanDpsl', 'ruse', 'etc'] - }, - 'rmk' - ] - } + /* 7. 기타 및 의견 */ + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, // sido -> ctpvNm + 'sggNm', + 'admdstNm' + ] + }, + 'displayYear', + 'wholFcltPrlg', + { + name: 'drdgGroup', header: { text: '준설연장 및 개소수' }, + items: [ + { + name: 'pipeLenGroup', header: { text: '관로(m)' }, + items: ['wholSwagePpl', 'cbsysPplPrlg', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }] + }, + 'mnhlCnt', 'exlnDrtwRcvprpCnt', 'stofcbrOtflCnt' + ] + }, + 'drdgnPlanPrlg', 'drdgnTrgtCtrsDrdgnRt', 'wholPrlgCtrsDrdgnRt', + { name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgnAmt', 'drdgnQnt'] }, + { + name: 'processGroup', header: { text: '준설토사처리' }, + items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt'] + }, + 'rmrk' + ] +} ]; export const SewagePipeMaintainColumnInfoList = rawMaintainList.map(tab => ({ 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 index 0281e43bc..330403c73 100644 --- 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 @@ -100,4 +100,8 @@ export interface SewerPipeDrdgResponseData { } -export type SewerPipeDrdgApiResponse = ApiResponse; \ No newline at end of file +export type SewerPipeDrdgApiResponse = ApiResponse; + +export interface SewagePipeMtcService { + getSewerPipeMtcDrdgItem(params: SewerPipeMtcRequestParams): Promise; +} \ No newline at end of file From de6bb049273df869ffc6efb3d440d877e42db25a Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 11:03:53 +0900 Subject: [PATCH 040/113] =?UTF-8?q?refactor:=2002.=ED=95=98=EC=88=98?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=20=ED=91=9C=EC=A4=80=ED=99=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/SewagePipeRvw.tsx | 9 +- .../util/column/review/SewagePipeRvwColumn.ts | 992 +++++++++--------- ...statistics-sewage-pipe-review-interface.ts | 275 ++--- 3 files changed, 628 insertions(+), 648 deletions(-) diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index fab4d2cbc..77d3a5d88 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -96,12 +96,11 @@ const SewagePipeRvw = () => { if (tabKey === 'ST00020002') { response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current); + }else if(tabKey === 'ST00020003'){ + response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current); + }else if(tabKey === 'ST00020004'){ + response = await statisticsManagementApi.getSewagePipeDiamList(searchParamRef.current); } - // }else if(tabKey === 'ST00020003'){ - // response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current); - // }else if(tabKey === 'ST00020004'){ - // response = await statisticsManagementApi.getSewagePipeDiamList(searchParamRef.current); - // } if (response?.status === 'success' && response.data) { diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 1020ce3bc..ae445b22d 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -10,553 +10,525 @@ const rawList = [ tabLabel: '지역별 시설현황', rowStartNum: 4, fixedColumns: 2, - columns: [ + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] - }, - - /* 하수정비 기본계획 */ - { name: 'targetYr', fieldName: 'targetYr', header: { text: '하수정보기본계획목표년도' } }, - { name: 'swrgPipeMntnPlanExtn', fieldName: 'swrgPipeMntnPlanExtn', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'swrgPipePlanExtn', fieldName: 'swrgPipePlanExtn', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'swrgPipeFcltyExtn', fieldName: 'swrgPipeFcltyExtn', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'swrgPipePsplyRt', fieldName: 'swrgPipePsplyRt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 합류식(m) */ - { name: 'cmbnMntnPlanAr', fieldName: 'cmbnMntnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'cmbnMntnPlanExtn', fieldName: 'cmbnMntnPlanExtn', 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: 'pltwtrMntnPlanAr', fieldName: 'pltwtrMntnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'pltwtrMntnPlanExtn', fieldName: 'pltwtrMntnPlanExtn', 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: 'pltwtrFcltyExtn', fieldName: 'pltwtrFcltyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrRctglExtn', fieldName: 'pltwtrRctglExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'pltwtrCicleExtn', fieldName: 'pltwtrCicleExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 분류식 - 우수관로 */ - { name: 'rainwtrMntnPlanAr', fieldName: 'rainwtrMntnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'rainwtrMntnPlanExtn', fieldName: 'rainwtrMntnPlanExtn', 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: 'rainwtrFcltyExtn', fieldName: 'rainwtrFcltyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrRctglExtn', fieldName: 'rainwtrRctglExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rainwtrCicleExtn', fieldName: 'rainwtrCicleExtn', 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: 'cmbnMnhleCnt', fieldName: 'cmbnMnhleCnt', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrMnhleCnt', fieldName: 'pltwtrMnhleCnt', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrMnhleCnt', fieldName: 'rainwtrMnhleCnt', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'pltwtrRcvrCnt', fieldName: 'pltwtrRcvrCnt', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrRcvrCnt', fieldName: 'rainwtrRcvrCnt', header: { text: '우수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrChmbrCnt', fieldName: 'rainwtrChmbrCnt', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'rainwtrOutltCnt', fieldName: 'rainwtrOutltCnt', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 대장 및 기타 */ - { name: 'swrgRegstrCmptExtn', fieldName: 'swrgRegstrCmptExtn', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'swrgRegstrCmptRt', fieldName: 'swrgRegstrCmptRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'areaGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'sggNm', - 'admdstNm', - ] - }, - 'displayYear', - 'targetYr', 'swrgPipeMntnPlanExtn', 'swrgPipePlanExtn', 'swrgPipeFcltyExtn', 'swrgPipePsplyRt', - { - name: 'combinedGroup', header: { text: '합류식(m)' }, - items: ['cmbnMntnPlanAr', 'cmbnMntnPlanExtn', 'cmbnPlanExtn', 'cmbnFcltyExtn', - { name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cmbnRctglExtn', 'cmbnCicleExtn'] }, - 'cmbnOpswrExtn', 'cmbnSdgttrExtn' - ], - }, - { - name: 'sepGroup', header: { text: '분류식(m)' }, - items: [ - { - name: 'foulPipeGroup', header: { text: '오수관로' }, - items: ['pltwtrMntnPlanAr', 'pltwtrMntnPlanExtn', 'pltwtrPlanExtn', 'pltwtrFcltyExtn', - { name: 'foulAmGroup', header: { text: '암거' }, items: ['pltwtrRctglExtn', 'pltwtrCicleExtn'] }] - }, - { - name: 'rainPipeGroup', header: { text: '우수관로' }, - items: ['rainwtrMntnPlanAr', 'rainwtrMntnPlanExtn', 'rainwtrPlanExtn', 'rainwtrFcltyExtn', - { name: 'rainAmGroup', header: { text: '암거' }, items: ['rainwtrRctglExtn', 'rainwtrCicleExtn'] }, - 'rainwtrOpswrExtn', 'rainwtrSdgttrExtn'] - } - ], - }, - 'cmbnMnhleCnt', - { name: 'fcltyGroup', header: { text: '부속시설(개소)' }, items: ['pltwtrMnhleCnt', 'rainwtrMnhleCnt'] }, - 'pltwtrRcvrCnt', 'rainwtrRcvrCnt', 'rainwtrChmbrCnt', 'rainwtrOutltCnt', 'swrgRegstrCmptExtn', 'swrgRegstrCmptRt', 'rmrk' + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + } ] }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { - tabKey: 'ST00020002', - tabLabel: '시설별 설치현황', - rowStartNum: 4, - fixedColumns: 3, - columns: [ + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - 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) => { - 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] + ' 합계' : '합계'; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, - { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] - }, - { name: 'swrmtFcltyCpc', fieldName: 'swrmtFcltyCpc', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'procZoneArSm', fieldName: 'procZoneArSm', 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: 'sprsysProcZoneAr', fieldName: 'sprsysProcZoneAr', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { 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: 'sprsysDrngEqpSm', fieldName: 'sprsysDrngEqpSm', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'swrgPipeFcltyExtn', fieldName: 'swrgPipeFcltyExtn', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'icgPltwtrMnnSm', fieldName: 'icgPltwtrMnnSm', 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: 'pltwtrMnnExtn', fieldName: 'pltwtrMnnExtn', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cmbnRainwtrChmbrCnt', fieldName: 'cmbnRainwtrChmbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysRainwtrChmbrCnt', fieldName: 'sprsysRainwtrChmbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysRainwtrOutltCnt', fieldName: 'sprsysRainwtrOutltCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { 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: 'sprsysExlnBrnchlnExtn', fieldName: 'sprsysExlnBrnchlnExtn', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'sprsysDrtwBrnchlnExtn', fieldName: 'sprsysDrtwBrnchlnExtn', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'sggNm', - 'admdstNm', - 'swrmtFcltyNm', - ] - }, - 'swrmtFcltyCpc', - 'displayYear', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['procZoneArSm', 'cmbnProcZoneAr', 'sprsysProcZoneAr'] }, - { - name: 'drainGroup', header: { text: '배수설비(개소)' }, - items: ['drngEqpSm', 'cmbnDrngEqp', 'sprsysDrngEqpSm'], - }, - { - name: 'pipeGroup', header: { text: '하수관로' }, - items: [ - 'swrgPipeFcltyExtn', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['icgPltwtrMnnSm', 'icgExtn', 'pltwtrMnnExtn'] }, - { - name: 'siteGroup', header: { text: '토실/토구(개소)' }, - items: [ - { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cmbnRainwtrChmbrCnt'] }, - { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysRainwtrChmbrCnt', 'sprsysRainwtrOutltCnt'] } - ] - }, - { - name: 'branchGroup', header: { text: '지선(m)' }, - items: ['brnchlnSm', 'cmbnBrnchlnExtn', { name: 'sepBranchGroup', header: { text: '우수/오수' }, items: ['sprsysExlnBrnchlnExtn', 'sprsysDrtwBrnchlnExtn'] }] - } - ], - }, - 'rmrk' + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + } ] }, + + /* 하수정비 기본계획 및 총계 */ + { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정보기본계획목표년도' } }, + { name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 합류식 */ + { name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 분류식 - 오수관로 */ + { name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 분류식 - 우수관로 */ + { name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 부속시설 */ + { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '우수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 대장 및 기타*/ + { name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, +], + columnLayout: [ + { + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', + 'admdstNm', + ] + }, + 'displayYear', + 'goalYr', 'wholPplMtncPlanPrlg', 'wholPlanPrlg', 'wholFcltPrlg', 'swagePplPsplyRt', + { + name: 'combinedGroup', header: { text: '합류식(m)' }, + items: ['cbsysPplMtncPlanArea', 'cbsysPplMtncPlanPrlg', 'cbsysPlanPrlg', 'cbsysFcltPrlg', + { name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'] }, + 'cbsysOpnPrlg', 'cbsysGttrPrlg' + ], + }, + { + name: 'sepGroup', header: { text: '분류식(m)' }, + items: [ + { + name: 'foulPipeGroup', header: { text: '오수관로' }, + items: ['drtwPplMtncPlanArea', 'drtwPplMtncPlanPrlg', 'drtwPplPlanPrlg', 'drtwPplFcltPrlg', + { name: 'foulAmGroup', header: { text: '암거' }, items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'] }] + }, + { + name: 'rainPipeGroup', header: { text: '우수관로' }, + items: ['exlnPplMtncPlanArea', 'exlnPplMtncPlanPrlg', 'exlnPplPlanPrlg', 'exlnPplFcltPrlg', + { name: 'rainAmGroup', header: { text: '암거' }, items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'] }, + 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'] + } + ], + }, + 'cbsysMnhlCnt', + { name: 'fcltyGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] }, + 'drtwRcvprpCnt', 'exlnRcvprpCnt', 'stofcbrCnt', 'exlnOtflCnt', 'sewerLdgrEdpPrlg', 'sewerLdgrEdpRt', 'rmrk' + ] + }, + { + tabKey: 'ST00020002', + tabLabel: '시설별 설치현황', + rowStartNum: 4, + fixedColumns: 4, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + 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) uniqueValues.add(String(divisionValue)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + 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) uniqueValues.add(String(divisionValue)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; + } + } + ] + }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; + } + } + ] + }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sprsysVartnRt', fieldName: 'sprsysVartnRt', header: { text: '분류식화' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'mltvEqpmCnt', fieldName: 'mltvEqpmCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cbsysMltvEqpmCnt', fieldName: 'cbsysMltvEqpmCnt', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysMltvEqpmCnt', fieldName: 'sprsysMltvEqpmCnt', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholIcptgAtlfcPrlg', fieldName: 'wholIcptgAtlfcPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'icptgPrlg', fieldName: 'icptgPrlg', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'atlfcsPrlg', fieldName: 'atlfcsPrlg', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysStofcbrCnt', fieldName: 'cbsysStofcbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysStofcbrCnt', fieldName: 'sprsysStofcbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnOtflCnt', fieldName: 'sprsysExlnOtflCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'wholBrchlnPrlg', fieldName: 'wholBrchlnPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysBrchlnPrlg', fieldName: 'cbsysBrchlnPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnBrchlnPrlg', fieldName: 'exlnBrchlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwBrchlnPrlg', fieldName: 'drtwBrchlnPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, +], + columnLayout: [ + { + name: 'addrGroup', header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', 'admdstNm' + ] + }, + 'swtrmtFcltNm','prcsFcltCpct', 'displayYear', + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, // name 변경됨 + 'sprsysVartnRt', + { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, // name 변경됨 + { + name: 'pipeGroup', header: { text: '하수관로' }, + items: [ + 'wholFcltPrlg', + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, // name 변경됨 + { + name: 'siteGroup', header: { text: '토실/토구(개소)' }, + items: [ + { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cbsysStofcbrCnt'] }, + { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysStofcbrCnt', 'sprsysExlnOtflCnt'] } + ] + }, + { + name: 'branchGroup', header: { text: '지선(m)' }, + items: ['wholBrchlnPrlg', 'cbsysBrchlnPrlg', { name: 'sepBranchGroup', header: { text: '분류식' }, items: ['exlnBrchlnPrlg', 'drtwBrchlnPrlg'] }] + } + ], + }, + 'rmrk' +] + }, { tabKey: 'ST00020003', tabLabel: '관종별 현황', rowStartNum: 2, - fixedColumns: 3, - columns: [ + fixedColumns: 2, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] - }, - - /* 총계 필드 */ - { name: 'totalExtnSum', fieldName: 'totalExtnSum', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'layingExtnSum', fieldName: 'layingExtnSum', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'remvlExtnSum', fieldName: 'remvlExtnSum', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - // const yrOffset = 29 - i; - return [ - { name: `totalExtn${yNum}y`, fieldName: `totalExtn${yNum}y`, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `layingExtn${yNum}y`, fieldName: `layingExtn${yNum}y`, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `remvlExtn${yNum}y`, fieldName: `remvlExtn${yNum}y`, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } - ]; - }).flat(), - - { name: 'layingExtn', fieldName: 'layingExtn', header: { text: '매설연장(m)\n(당해년도)' }, 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', - 'swrgPipeNm', - ] - }, - 'displayYear', - { name: 'summaryGroup', header: { text: '총계' }, items: ['totalExtnSum', 'layingExtnSum', 'remvlExtnSum'] }, - - ...Array.from({ length: 30 }, (_, i) => { - const yNum = (30 - i).toString().padStart(2, '0'); - const yrOffset = 29 - i; - return { - name: `yr${yNum}Group`, - header: { text: `-${yrOffset}년` }, - items: [`totalExtn${yNum}y`, `layingExtn${yNum}y`, `remvlExtn${yNum}y`] - }; - }), - - { name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['layingExtn'] }, - 'rmk' + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + } ] }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + } + ] + }, + + { name: 'pipeByPrstWholSum', fieldName: 'pipeByPrstWhol', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + /* 동적 연도 컬럼 (1996년이전[yy30] ~ 2025년[yy1]) */ + ...Array.from({ length: 30 }, (_, i) => { + const dtoNum = 30 - i; // 30, 29, ..., 1 + + const wholField = `yy${dtoNum}PrlgWhol`; + const lyngField = `yy${dtoNum}LyngPrlg`; + const dmltnField = `yy${dtoNum}DmltnPrlg`; + + return [ + { name: `${wholField}_col`, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `${lyngField}_col`, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: `${dmltnField}_col`, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + ]; + }).flat(), + + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ],columnLayout: [ + { name: 'areaGroup', header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'swrgPipeNm'] }, + 'displayYear', + { name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum','lyngPrlgSum', 'dmltnPrlgSum'] }, + + ...Array.from({ length: 30 }, (_, i) => { + const dtoNum = 30 - i; + const yearLabel = i === 0 ? '1996년이전' : `${1995 + i}년`; + const wholField = `yy${dtoNum}PrlgWhol`; + + return { + name: `group_${yearLabel}`, + header: { text: yearLabel }, + items: [`${wholField}_col`, `yy${dtoNum}LyngPrlg_col`, `yy${dtoNum}DmltnPrlg_col`] + }; + }), + 'rmrk' + ] + }, { tabKey: 'ST00020004', tabLabel: '관경별 현황', rowStartNum: 2, fixedColumns: 3, - columns: [ + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ { - name: 'sido', - fieldName: 'sido', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - 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) => { - 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] + '합계' : '합계'; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - 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) => { - 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] + '합계' : ''; - } + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); } - ] - }, - - { name: 'totalExtnM', fieldName: 'totalExtnM', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'under200', fieldName: 'under200', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over200Undev300', fieldName: 'over200Undev300', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over300Undev500', fieldName: 'over300Undev500', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over500Undev500', fieldName: 'over500Undev500', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'over800', fieldName: 'over800', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'box', fieldName: 'box', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'etc', fieldName: 'etc', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, - ], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'gugun', - 'swrgPipeNm', - ] - }, - 'displayYear', - 'totalExtnM', - { - name: 'circleGroup', - header: { text: '원형(m)' }, - items: ['under200', 'over200Undev300', 'over300Undev500', 'over500Undev500', 'over800'] - }, - 'box', - 'etc', - 'rmk' + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + } ] + }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, // gugun -> sggNm + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + } + ] + }, + + /* 관경별 수치 필드 */ + { name: 'pdmtByTsmWhol', fieldName: 'pdmtByTsmWhol', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step1PdmtPrlg', fieldName: 'step1PdmtPrlg', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step2PdmtPrlg', fieldName: 'step2PdmtPrlg', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step3PdmtPrlg', fieldName: 'step3PdmtPrlg', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step4PdmtPrlg', fieldName: 'step4PdmtPrlg', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step5PdmtPrlg', fieldName: 'step5PdmtPrlg', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sqrePdmtPrlg', fieldName: 'sqrePdmtPrlg', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etcPdmtPrlg', fieldName: 'etcPdmtPrlg', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // rmk -> rmrk +], + columnLayout: [ + { + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', + 'admdstNm', + ] + }, + 'swrgPipeNm', + 'displayYear', + 'pdmtByTsmWhol', + { + name: 'circleGroup', + header: { text: '원형(m)' }, + items: ['step1PdmtPrlg', 'step2PdmtPrlg', 'step3PdmtPrlg', 'step4PdmtPrlg', 'step5PdmtPrlg'] // 이름 일괄 변경 + }, + 'sqrePdmtPrlg', + 'etcPdmtPrlg', + 'rmrk' +] } ]; 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 3d97ec558..6ffa91466 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 @@ -4,65 +4,62 @@ import type { ApiResponse } from "./statistics-interface"; * 02. 하수관로 tab1.지역별 시설현황 아이템 인터페이스 (Tab1) */ export interface SewagePipeReviewrRegionalItem { + ctpvNm: string; // 시도명 + sggNm: string; // 시군구명 + admdstNm: string; // 행정구역명 + admdstCd: string; // 행정구역코드 + statsYr: string; // 통계 연도 + displayYear: string; // 화면 표시용 연도 + rowType: 'OLD' | 'CURR'; // 데이터 구분 -/* --- 기본 정보 및 행 제어 --- */ - ctpvNm: string; // 시도명 - sggNm: string; // 시군구명 - admdstNm: string; // 행정구역명 - admdstCd: string; // 행정구역코드 - statsYr: string; // 통계 연도 - rowType: 'OLD' | 'CURR'; // 데이터 구분 (OLD: 확정분, CURR: 당해년도) - displayYear: string; // 화면 표시용 연도 + /* --- 하수정비 기본계획 및 총계 --- */ + goalYr: string; // 하수정비 기본계획 목표연도 + wholPplMtncPlanPrlg: number; // 총관로정비계획연장 + wholPlanPrlg: number; // 전체계획연장 + wholFcltPrlg: number; // 전체시설연장 + swagePplPsplyRt: number; // 하수관로보급비율 - /* --- 하수정비 기본계획 --- */ - targetYr: number; // 하수정보기본계획 목표년도 - swrgPipeMntnPlanExtn: number; // 총 관로정비 계획연장(m) - swrgPipePlanExtn: number; // 총 계획연장(m) - swrgPipeFcltyExtn: number; // 총 시설연장(m) - swrgPipePsplyRt: number; // 하수관로 보급률(%) + /* --- 합류식 (Combined) --- */ + cbsysPplMtncPlanArea: number; // 합류식관로정비계획면적 + cbsysPplMtncPlanPrlg: number; // 합류식관로정비계획연장 + cbsysPlanPrlg: number; // 합류식계획연장 + cbsysFcltPrlg: number; // 합류식시설연장 + cbsysSqrePrlg: number; // 합류식사각연장 + cbsysCiclPrlg: number; // 합류식원형연장 + cbsysOpnPrlg: number; // 합류식개방연장 + cbsysGttrPrlg: number; // 합류식측구연장 - /* --- 합류식 (Combined System) --- */ - cmbnMntnPlanAr: number; // 관로정비 계획면적(㎢) - cmbnMntnPlanExtn: number; // 관로정비 계획연장(m) - cmbnPlanExtn: number; // 계획연장(m) - cmbnFcltyExtn: number; // 시설연장(m) - cmbnRctglExtn: number; // 사각형 암거 연장(m) - cmbnCicleExtn: number; // 원형 암거 연장(m) - cmbnOpswrExtn: number; // 개거 연장(m) - cmbnSdgttrExtn: number; // 측구 연장(m) + /* --- 분류식 - 오수관로 (Foul Water) --- */ + drtwPplMtncPlanArea: number; // 오수관로정비계획면적 + drtwPplMtncPlanPrlg: number; // 오수관로정비계획연장 + drtwPplPlanPrlg: number; // 오수관로계획연장 + drtwPplFcltPrlg: number; // 오수관로시설연장 + drtwPplSqrePrlg: number; // 오수관로사각연장 + drtwPplCiclPrlg: number; // 오수관로원형연장 - /* --- 분류식 - 오수관로 (Foul Water Pipe) --- */ - pltwtrMntnPlanAr: number; // 관로정비 계획면적(㎢) - pltwtrMntnPlanExtn: number; // 관로정비 계획연장(m) - pltwtrPlanExtn: number; // 계획연장(m) - pltwtrFcltyExtn: number; // 시설연장(m) - pltwtrRctglExtn: number; // 사각형 암거 연장(m) - pltwtrCicleExtn: number; // 원형 암거 연장(m) + /* --- 분류식 - 우수관로 (Rain Water) --- */ + exlnPplMtncPlanArea: number; // 우수관로정비계획면적 + exlnPplMtncPlanPrlg: number; // 우수관로정비계획연장 + exlnPplPlanPrlg: number; // 우수관로계획연장 + exlnPplFcltPrlg: number; // 우수관로시설연장 + exlnPplSqrePrlg: number; // 우수관로사각연장 + exlnPplCiclPrlg: number; // 우수관로원형연장 + exlnPplOpnPrlg: number; // 우수관로개방연장 + exlnPplGttrPrlg: number; // 우수관로측구연장 - /* --- 분류식 - 우수관로 (Rain Water Pipe) --- */ - rainwtrMntnPlanAr: number; // 관로정비 계획면적(㎢) - rainwtrMntnPlanExtn: number; // 관로정비 계획연장(m) - rainwtrPlanExtn: number; // 계획연장(m) - rainwtrFcltyExtn: number; // 시설연장(m) - rainwtrRctglExtn: number; // 사각형 암거 연장(m) - rainwtrCicleExtn: number; // 원형 암거 연장(m) - rainwtrOpswrExtn: number; // 개거 연장(m) - rainwtrSdgttrExtn: number; // 측구 연장(m) - - /* --- 부속시설 (개소) --- */ - cmbnMnhleCnt: number; // 합류식 맨홀 수 - pltwtrMnhleCnt: number; // 오수 맨홀 수 - rainwtrMnhleCnt: number; // 우수 맨홀 수 - pltwtrRcvrCnt: number; // 오수받이 수 - rainwtrRcvrCnt: number; // 우수받이(빗물받이) 수 - rainwtrChmbrCnt: number; // 우수토실 수 - rainwtrOutltCnt: number; // 우수토구 수 + /* --- 부속시설 --- */ + cbsysMnhlCnt: number; // 합류식맨홀수 + sprsysDrtwMnhlCnt: number; // 분류식오수맨홀수 + sprsysExlnMnhlCnt: number; // 분류식우수맨홀수 + drtwRcvprpCnt: number; // 오수받이수 + exlnRcvprpCnt: number; // 우수받이수 + stofcbrCnt: number; // 우수토실수 + exlnOtflCnt: number; // 우수토구수 /* --- 대장 및 기타 --- */ - swrgRegstrCmptExtn: number; // 하수도대장 전산화 연장(m) - swrgRegstrCmptRt: number; // 하수도대장 전산화율(%) - rmrk: string; // 비고 - + sewerLdgrEdpPrlg: number; // 하수도대장전산화연장 + sewerLdgrEdpRt: number; // 하수도대장전산화비율 + rmrk: string | null; // 비고 } export interface SewagePipeReviewResponseData { @@ -70,10 +67,10 @@ export interface SewagePipeReviewResponseData { } export interface SewagePipeReviewRequestParams { - ctpvNm?: string; // 시도명 - sggNm?: string; // 시군구명 - statsYr?: string; // 통계 연도 - sewerBzmnCd?: string; // 하수도사업자 코드 + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; } export type SewagePipeReviewRegionalApiResponse = ApiResponse & { @@ -89,47 +86,48 @@ export interface SewagePipeReviewService { */ export interface SewagePipeReviewFacilityItem { // 1. 행정구역 및 기본 정보 - ctpvNm: string; // 시도명 - sggNm: string; // 시군구명 - admdstCd: string; // 행정구역코드 - admdstNm: string; // 행정구역명 - statsYr: string; // 통계연도 - rowType: 'OLD' | 'CURR'; // 데이터 구분 (OLD: 확정, CURR: 당해년도) - displayYear: string; // 화면 표시용 연도 + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string; + statsYr: string; + rowType: 'OLD' | 'CURR'; + displayYear: string; // 2. 시설 정보 - swrmtFcltyNm: string; // 하수처리시설 명칭 - swrmtFcltyCpc: number; // 하수처리시설 용량(㎥/일) + swtrmtFcltNm: string; + prcsFcltCpct: number; // 3. 처리구역 면적(㎢) - procZoneArSm: number; // 처리구역 면적 합계 - cmbnProcZoneAr: number; // 합류식 처리구역 면적 - sprsysProcZoneAr: number; // 분류식 처리구역 면적 + prcsZoneArea: number; + cbsysPrcsZoneArea: number; + sprsysPrcsZoneArea: number; + + sprsysVartnRt:number;//분류식 // 4. 배수설비(개소) - drngEqpSm: number; // 배수설비 합계 - cmbnDrngEqp: number; // 합류식 배수설비 - sprsysDrngEqpSm: number; // 분류식 배수설비 합계 + mltvEqpmCnt: number; + cbsysMltvEqpmCnt: number; + sprsysMltvEqpmCnt: number; // 5. 하수관로 (m) - swrgPipeFcltyExtn: number; // 총 시설연장 - icgPltwtrMnnSm: number; // 차집 및 오수간선 합계 - icgExtn: number; // 차집관로 연장 - pltwtrMnnExtn: number; // 오수간선 연장 + wholFcltPrlg: number; + wholIcptgAtlfcPrlg: number; + icptgPrlg: number; + atlfcsPrlg: number; // 6. 우수토실/토구 (개소) - cmbnRainwtrChmbrCnt: number; // 합류식 우수토실 - sprsysRainwtrChmbrCnt: number; // 분류식 우수토실 - sprsysRainwtrOutltCnt: number; // 분류식 우수토구 + cbsysStofcbrCnt: number; + sprsysStofcbrCnt: number; + sprsysExlnOtflCnt: number; // 7. 지선 (m) - brnchlnSm: number; // 지선 합계 - cmbnBrnchlnExtn: number; // 합류식 지선연장 - sprsysExlnBrnchlnExtn: number; // 분류식 우수지선연장 - sprsysDrtwBrnchlnExtn: number; // 분류식 오수지선연장 + wholBrchlnPrlg: number; + cbsysBrchlnPrlg: number; + exlnBrchlnPrlg: number; + drtwBrchlnPrlg: number; - // 비고 - rmrk: string | null; // 비고 + rmrk: string | null; } @@ -152,29 +150,22 @@ export interface SewagePipeFacilityService { * */ export interface SewerPipeTypeItem { - // --- SewerPipeTab3Response 기본 필드 --- - sido: string; // 시도 - gugun: string; // 시군구 - adstCd: string; // 행정코드 - swrgPipeNm: string; // 관종명 - displayYear: string; // 구분 표시용 (예: 2024년확정) - yr: string; // 연도 - rmk: string; // 비고 - yrGbn?: string; // 구분 (매설/철거 등 행 구분용 - 그리드 설정 기준) + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string; + swrgPipeNm: string; + statsYr: string; + displayYear: string; + rowType: 'OLD' | 'CURR'; + rmrk: string | null; - // --- BasePipeExtnResponse 상속 필드 --- - layingExtn: number; // 매설연장(m) - 당해년도 + // 총합 필드 + pipeByPrstWhol:number; + lyngPrlg: number; + dmltnPrlg: number; - // 총계(Sum) 필드 (그리드 설정에서 사용하는 합계용 필드) - totalExtnSum?: number; - layingExtnSum?: number; - remvlExtnSum?: number; - - // 01y ~ 30y 동적 필드 처리 (Index Signature) - // 예: totalExtn01y, layingExtn05y, remvlExtn30y 등 - [key: `totalExtn${string}y`]: number | undefined; - [key: `layingExtn${string}y`]: number | undefined; - [key: `remvlExtn${string}y`]: number | undefined; + [key: string]: string | number | null | undefined; } @@ -182,37 +173,60 @@ export interface SewerPipeTypeResponseData { pipeTypeStatusList: SewerPipeTypeItem[]; } -export type SewerPipeTypeApiResponse = ApiResponse; +export type SewerPipeTypeApiResponse = ApiResponse; export interface SewagePipeTypeService { - getSewagePipeTypeList(params: { - yr?: string; // 조회 연도 - bsnmCd?: string; // 사업장 코드 - sido?: string; // 시도 - gugun?: string; // 구군 - }): Promise; + getSewagePipeTypeList(params: SewagePipeReviewRequestParams): Promise; } /** * 02. 하수관로 관경별 시설현황 아이템 (Tab 4) */ export interface SewerPipeDiamItem { - sido: string; // 시도 - gugun: string; // 시군구 - adstCd: string; // 행정코드 - swrgPipeNm: string; // 관종명 (또는 관경 관련 명칭) - displayYear: string; // 구분 (예: 2023년 확정 / 2024년) - - // 관경별 수치 데이터 - under200: number; // 200미만 - over200Undev300: number; // 200~300 - over300Undev500: number; // 300~500 - over500Undev500: number; // 500~800 (DTO 명칭 기준) - over800: number; // 800이상 - box: number; // BOX - etc: number; // 기타 - rmk: string; // 비고 + // 1. 기본 정보 + ctpvNm: string; // 시도명 + sggNm: string; // 시군구명 + admdstNm: string; // 행정구역명 + admdstCd: string; // 행정구역코드 + statsYr: string; // 통계연도 (statsYr로 통일) + rowType: 'OLD' | 'CURR'; // 데이터 구분 + displayYear: string; // 화면 표시용 연도 + sewerBzmnCd: string; // 하수도사업자코드 + + // 2. 시설 현황 + wholFcltPrlg: number; // 전체시설연장 (m) + + // 3. 준설연장 및 개소수 - 관로 (m) + wholSwagePpl: number; // 전체하수관로 준설연장 (m) + cbsysPplPrlg: number; // 합류식관로연장 (m) + sprsysDrtwPrlg: number; // 분류식오수연장 (m) + sprsysExlnPrlg: number; // 분류식우수연장 (m) + + // 4. 준설연장 및 개소수 - 부속시설 (개소) + mnhlCnt: number; // 맨홀수 + exlnDrtwRcvprpCnt: number; // 우수오수받이수 + stofcbrOtflCnt: number; // 우수토실토구수 + + // 5. 준설 효율 및 성과 + drdgnPlanPrlg: number; // 준설계획연장 (m) + drdgnTrgtCtrsDrdgnRt: number; // 준설대상대비준설비율 (%) + wholPrlgCtrsDrdgnRt: number; // 전체연장대비준설비율 (%) + + // 6. 준설실적 + drdgnAmt: number; // 준설금액 (백만원) + drdgnQnt: number; // 준설량 (m3) + + // 7. 준설토사처리 (톤) + drdgnUntprcAmt: number; // 준설단가금액 (원/m3) + wholDrdgnPrcsQnt: number; // 전체준설처리량 (톤) + rclmQnt: number; // 매립량 + oceanEmsnQnt: number; // 해양방출량 + rcycQnt: number; // 재활용량 + etcQnt: number; // 기타량 + + // 8. 기타 + rmrk: string; // 비고 } @@ -225,10 +239,5 @@ export type SewerPipeDiamApiResponse = ApiResponse; export interface SewagePipeDiamService { - getSewagePipeDiamList(params: { - yr?: string; // 조회 연도 - bsnmCd?: string; // 사업장 코드 - sido?: string; // 시도 - gugun?: string; // 구군 - }): Promise; + getSewagePipeDiamList(params: SewagePipeReviewRequestParams): Promise; } \ No newline at end of file From d225e9fcacad129a534c62978140a20ee2ad64fc Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 11 Mar 2026 11:11:04 +0900 Subject: [PATCH 041/113] =?UTF-8?q?refactor:=2005.=ED=8E=8C=ED=94=84?= =?UTF-8?q?=EC=9E=A5=20=EB=A8=B8=EC=A7=80=EC=85=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/column/review/PumpingStationRvwColumn.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index 9f40941f3..3c22c517c 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -88,12 +88,12 @@ export const rawPumpingStationInfoList = { ] }, { name: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true }, - { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' }, mergeCells: true }, // bscFctNm -> swtrmtFcltNm + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, // bscFctNm -> swtrmtFcltNm { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, // pumpCls -> pumpSeNm { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, // addr -> impdFcltPstnCn { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, // rnDt -> oprtngStrtYmd { - name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' }, // planDrhgAr -> planMltvArea + name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, @@ -147,11 +147,11 @@ export const rawPumpingStationInfoList = { name: 'groupA', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', - 'pumpstNm', ] }, + 'pumpstNm', 'displayYear', 'swtrmtFcltNm', 'pumpSeNm', @@ -192,7 +192,7 @@ export const rawPumpingStationInfoList = { 'anlElpwrUseQnt', 'rmrk', ], - fixedColumns: 2, + fixedColumns: 3, rowStartNum: 3 }; From 42a99708a335428cfcf3eb57f90fe195677a494b Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 11 Mar 2026 12:51:31 +0900 Subject: [PATCH 042/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + AGENTS.md | 1 + .../verification/HbStatisticsVerification.tsx | 2 + .../detail/HbVerificationDetail.tsx | 16 +- .../verification/detail/RegionalGrid.tsx | 143 +++++++++++------- .../verification/detail/VerificationList.tsx | 35 ++--- .../statistics/api/analysis-history-api.ts | 45 +++--- .../model/analysis-history-interface.ts | 7 +- .../statistics-analysis-result-interface.ts | 2 +- 9 files changed, 147 insertions(+), 108 deletions(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index c5082bba3..e4fab5cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,9 @@ pnpm-lock.yaml settings.json .npmrc .cursor +.agent apps/**/tsconfig.tsbuildinfo + +# next-agents-md +.next-docs/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1da991844 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +[Next.js Docs Index]|root: ./.next-docs|STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.|01-app/01-getting-started:{01-installation.mdx,02-project-structure.mdx,03-layouts-and-pages.mdx,04-images-and-fonts.mdx,05-css.mdx,06-fetching-data.mdx,07-updating-data.mdx,08-error-handling.mdx,10-metadata-and-og-images.mdx,12-upgrading.mdx}|01-app/03-building-your-application/01-routing:{03-layouts-and-templates.mdx,04-linking-and-navigating.mdx,05-error-handling.mdx,06-loading-ui-and-streaming.mdx,07-redirecting.mdx,08-route-groups.mdx,10-dynamic-routes.mdx,11-parallel-routes.mdx,12-intercepting-routes.mdx,13-route-handlers.mdx,14-middleware.mdx,15-internationalization.mdx}|01-app/03-building-your-application/02-data-fetching:{01-fetching.mdx,03-server-actions-and-mutations.mdx,04-incremental-static-regeneration.mdx}|01-app/03-building-your-application/03-rendering:{01-server-components.mdx,02-client-components.mdx,03-composition-patterns.mdx,04-partial-prerendering.mdx,05-edge-and-nodejs-runtimes.mdx}|01-app/03-building-your-application/05-styling:{01-css.mdx,02-tailwind-css.mdx,03-sass.mdx,04-css-in-js.mdx}|01-app/03-building-your-application/06-optimizing:{01-images.mdx,02-videos.mdx,03-fonts.mdx,04-metadata.mdx,05-scripts.mdx,06-package-bundling.mdx,07-lazy-loading.mdx,08-analytics.mdx,09-instrumentation.mdx,10-open-telemetry.mdx,11-static-assets.mdx,12-third-party-libraries.mdx,13-memory-usage.mdx,14-local-development.mdx}|01-app/03-building-your-application/07-configuring:{03-environment-variables.mdx,05-mdx.mdx,06-src-directory.mdx,10-custom-server.mdx,11-draft-mode.mdx,15-content-security-policy.mdx,16-debugging.mdx,16-progressive-web-apps.mdx}|01-app/03-building-your-application/08-testing:{01-vitest.mdx,02-jest.mdx,03-playwright.mdx,04-cypress.mdx}|01-app/03-building-your-application/10-deploying:{01-production-checklist.mdx,02-static-exports.mdx,03-multi-zones.mdx}|01-app/03-building-your-application/11-upgrading:{01-codemods.mdx,02-canary.mdx,03-version-15.mdx,04-version-14.mdx,05-app-router-migration.mdx,06-from-create-react-app.mdx,07-from-vite.mdx,08-single-page-applications.mdx}|01-app/05-api-reference:{07-edge.mdx,08-turbopack.mdx}|01-app/05-api-reference/01-directives:{use-cache.mdx,use-client.mdx,use-server.mdx}|01-app/05-api-reference/02-components:{font.mdx,form.mdx,image.mdx,link.mdx,script.mdx}|01-app/05-api-reference/03-file-conventions/01-metadata:{app-icons.mdx,manifest.mdx,opengraph-image.mdx,robots.mdx,sitemap.mdx}|01-app/05-api-reference/03-file-conventions:{default.mdx,error.mdx,forbidden.mdx,instrumentation-client.mdx,instrumentation.mdx,layout.mdx,loading.mdx,mdx-components.mdx,middleware.mdx,not-found.mdx,page.mdx,route-segment-config.mdx,route.mdx,template.mdx,unauthorized.mdx}|01-app/05-api-reference/04-functions:{after.mdx,cacheLife.mdx,cacheTag.mdx,connection.mdx,cookies.mdx,draft-mode.mdx,fetch.mdx,forbidden.mdx,generate-image-metadata.mdx,generate-metadata.mdx,generate-sitemaps.mdx,generate-static-params.mdx,generate-viewport.mdx,headers.mdx,image-response.mdx,next-request.mdx,next-response.mdx,not-found.mdx,permanentRedirect.mdx,redirect.mdx,revalidatePath.mdx,revalidateTag.mdx,unauthorized.mdx,unstable_cache.mdx,unstable_noStore.mdx,unstable_rethrow.mdx,use-link-status.mdx,use-params.mdx,use-pathname.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,use-selected-layout-segment.mdx,use-selected-layout-segments.mdx,userAgent.mdx}|01-app/05-api-reference/05-config/01-next-config-js:{allowedDevOrigins.mdx,appDir.mdx,assetPrefix.mdx,authInterrupts.mdx,basePath.mdx,cacheLife.mdx,compress.mdx,crossOrigin.mdx,cssChunking.mdx,devIndicators.mdx,distDir.mdx,dynamicIO.mdx,env.mdx,eslint.mdx,expireTime.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,htmlLimitedBots.mdx,httpAgentOptions.mdx,images.mdx,incrementalCacheHandlerPath.mdx,inlineCss.mdx,logging.mdx,mdxRs.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,ppr.mdx,productionBrowserSourceMaps.mdx,reactCompiler.mdx,reactMaxHeadersLength.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,sassOptions.mdx,serverActions.mdx,serverComponentsHmrCache.mdx,serverExternalPackages.mdx,staleTimes.mdx,staticGeneration.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,typedRoutes.mdx,typescript.mdx,urlImports.mdx,useCache.mdx,useLightningcss.mdx,viewTransition.mdx,webVitalsAttribution.mdx,webpack.mdx}|01-app/05-api-reference/05-config:{02-typescript.mdx,03-eslint.mdx}|01-app/05-api-reference/06-cli:{create-next-app.mdx,next.mdx}|02-pages/01-getting-started:{01-installation.mdx,02-project-structure.mdx}|02-pages/02-building-your-application/01-routing:{01-pages-and-layouts.mdx,02-dynamic-routes.mdx,03-linking-and-navigating.mdx,04-redirecting.mdx,05-custom-app.mdx,06-custom-document.mdx,07-api-routes.mdx,08-custom-error.mdx,10-internationalization.mdx,11-middleware.mdx}|02-pages/02-building-your-application/02-rendering:{01-server-side-rendering.mdx,02-static-site-generation.mdx,04-automatic-static-optimization.mdx,05-client-side-rendering.mdx,06-edge-and-nodejs-runtimes.mdx}|02-pages/02-building-your-application/03-data-fetching:{01-get-static-props.mdx,02-get-static-paths.mdx,03-forms-and-mutations.mdx,03-get-server-side-props.mdx,04-incremental-static-regeneration.mdx,05-client-side.mdx}|02-pages/02-building-your-application/04-styling:{01-css.mdx,02-tailwind-css.mdx,03-css-in-js.mdx,04-sass.mdx}|02-pages/02-building-your-application/05-optimizing:{01-images.mdx,02-fonts.mdx,03-scripts.mdx,05-static-assets.mdx,06-package-bundling.mdx,07-analytics.mdx,08-lazy-loading.mdx,09-instrumentation.mdx,10-open-telemetry.mdx,11-third-party-libraries.mdx}|02-pages/02-building-your-application/06-configuring:{03-environment-variables.mdx,05-src-directory.mdx,06-mdx.mdx,07-amp.mdx,08-babel.mdx,09-post-css.mdx,10-custom-server.mdx,11-draft-mode.mdx,12-error-handling.mdx,13-preview-mode.mdx,14-content-security-policy.mdx,16-debugging.mdx}|02-pages/02-building-your-application/07-testing:{01-vitest.mdx,02-jest.mdx,03-playwright.mdx,04-cypress.mdx}|02-pages/02-building-your-application/09-deploying:{01-production-checklist.mdx,02-static-exports.mdx,03-multi-zones.mdx,04-ci-build-caching.mdx}|02-pages/02-building-your-application/10-upgrading:{01-codemods.mdx,02-app-router-migration.mdx,03-from-vite.mdx,04-from-create-react-app.mdx,05-version-14.mdx,06-version-13.mdx,07-version-12.mdx,08-version-11.mdx,09-version-10.mdx,10-version-9.mdx}|02-pages/03-api-reference:{06-edge.mdx,08-turbopack.mdx}|02-pages/03-api-reference/01-components:{font.mdx,form.mdx,head.mdx,image-legacy.mdx,image.mdx,link.mdx,script.mdx}|02-pages/03-api-reference/02-file-conventions:{instrumentation.mdx}|02-pages/03-api-reference/03-functions:{get-initial-props.mdx,get-server-side-props.mdx,get-static-paths.mdx,get-static-props.mdx,next-request.mdx,next-response.mdx,use-amp.mdx,use-report-web-vitals.mdx,use-router.mdx,userAgent.mdx}|02-pages/03-api-reference/04-config/01-next-config-js:{allowedDevOrigins.mdx,assetPrefix.mdx,basePath.mdx,bundlePagesRouterDependencies.mdx,compress.mdx,crossOrigin.mdx,devIndicators.mdx,distDir.mdx,env.mdx,eslint.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,httpAgentOptions.mdx,images.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,runtime-configuration.mdx,serverExternalPackages.mdx,trailingSlash.mdx,transpilePackages.mdx,turbo.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,webVitalsAttribution.mdx,webpack.mdx}|02-pages/03-api-reference/04-config:{01-typescript.mdx,02-eslint.mdx}|02-pages/03-api-reference/05-cli:{create-next-app.mdx,next.mdx}|03-architecture:{accessibility.mdx,fast-refresh.mdx,nextjs-compiler.mdx,supported-browsers.mdx}|04-community:{01-contribution-guide.mdx} diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 0f44b8e4a..4da13d0e2 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -180,6 +180,7 @@ const HbStatisticsVerification = () => { const params: StAnlsMstSearchParams = { page: historyPagination.pageIndex, size: historyPagination.pageSize, + sort: "anlsSn,desc", }; if (appliedHistoryDateRange?.from) { @@ -455,6 +456,7 @@ const HbStatisticsVerification = () => { diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 11eb6c8fd..90ffd5350 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -2,10 +2,10 @@ import React, { useState } from "react"; import { Button } from "@common/components"; -import AreaDIndicators from "./Indicators"; -import AreaBRegionalGrid from "./RegionalGrid"; -import AreaCScatterChart from "./ScatterChart"; -import AreaAVerificationList from "./VerificationList"; +import Indicators from "./Indicators"; +import RegionalGrid from "./RegionalGrid"; +import ScatterChart from "./ScatterChart"; +import VerificationList from "./VerificationList"; interface HbVerificationDetailProps { anlsSn: number; @@ -41,7 +41,7 @@ const HbVerificationDetail: React.FC = ({
- = ({

- = ({
-
- void; } +interface AppliedFilter { + sido: string | null; + gugun: string; + status: string; +} + const RegionalGrid: React.FC = ({ anlsSn, vrfcTrgtSn, selectedSido, onSidoSelect, }) => { - const [selectedGugun, setSelectedGugun] = useState("ALL"); - const [selectedStatus, setSelectedStatus] = useState("ALL"); + const [selectedGugun, setSelectedGugun] = useState(""); + const [selectedStatus, setSelectedStatus] = useState(""); + const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] = + useState(true); + const [appliedFilter, setAppliedFilter] = useState({ + sido: null, + gugun: "", + status: "", + }); const { data: regionCodes } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], @@ -51,7 +65,7 @@ const RegionalGrid: React.FC = ({ }, [regionCodes]); const gugunOptions = useMemo(() => { - if (!regionCodes || !selectedSido || selectedSido === "ALL") return []; + if (!regionCodes || !selectedSido) return []; const sidoPrefix = selectedSido; return regionCodes.filter( @@ -60,11 +74,7 @@ const RegionalGrid: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { - data: resultData, - isLoading, - refetch, - } = useQuery({ + const { data: resultData, isLoading } = useQuery({ queryKey: [ ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, "list", @@ -78,43 +88,49 @@ const RegionalGrid: React.FC = ({ page: 0, size: 1000, }), - enabled: false, + enabled: !!vrfcTrgtSn, }); const rowData = useMemo(() => { let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; - if (selectedSido && selectedSido !== "ALL") { + if ( + !appliedFilter.sido && + !appliedFilter.gugun && + !appliedFilter.status + ) { + return showAllOnEmptyFilter ? data : []; + } + + if (appliedFilter.sido) { const sidoName = sidoOptions.find( - (opt) => opt.lwpoCd === selectedSido, + (opt) => opt.lwpoCd === appliedFilter.sido, )?.lwpoCdNm; if (sidoName) { data = data.filter((item) => item.sido === sidoName); } } - if (selectedGugun && selectedGugun !== "ALL") { + if (appliedFilter.gugun) { const gugunName = gugunOptions.find( - (opt) => opt.lwpoCd === selectedGugun, + (opt) => opt.lwpoCd === appliedFilter.gugun, )?.lwpoCdNm; if (gugunName) { - data = data.filter((item) => item.gugun === gugunName); + data = data.filter((item) => gugunName.includes(item.gugun)); } } - if (selectedStatus && selectedStatus !== "ALL") { - const statusTarget = selectedStatus === "NORMAL" ? "y" : "n"; + if (appliedFilter.status) { data = data.filter( - (item) => item.otlrYn.toLowerCase() === statusTarget, + (item) => item.otlrYn.toLowerCase() === appliedFilter.status, ); } return data; }, [ resultData, - selectedSido, - selectedGugun, - selectedStatus, + appliedFilter, + showAllOnEmptyFilter, sidoOptions, gugunOptions, ]); @@ -150,19 +166,13 @@ const RegionalGrid: React.FC = ({ size: 60, cell: (info) => { const val = info.getValue(); - if (val === "y") - return ( - - 정상 - - ); - if (val === "n") - return ( - - 이상치 - - ); - return "-"; + return ( + + {val === "0" ? "정상" : "이상치"} + + ); }, }, ], @@ -180,8 +190,29 @@ const RegionalGrid: React.FC = ({ }); const handleSidoChange = (val: string) => { - onSidoSelect(val === "ALL" ? null : val); - setSelectedGugun("ALL"); + onSidoSelect(!val ? null : val); + setSelectedGugun(""); + }; + + const handleSearch = () => { + setAppliedFilter({ + sido: selectedSido, + gugun: selectedGugun, + status: selectedStatus, + }); + setShowAllOnEmptyFilter(false); + }; + + const handleReset = () => { + onSidoSelect(null); + setSelectedGugun(""); + setSelectedStatus(""); + setAppliedFilter({ + sido: null, + gugun: "", + status: "", + }); + setShowAllOnEmptyFilter(true); }; return ( @@ -189,14 +220,13 @@ const RegionalGrid: React.FC = ({
- 전체 (시군구) {gugunOptions.map((opt) => ( {opt.lwpoCdNm} @@ -226,41 +255,47 @@ const RegionalGrid: React.FC = ({ onValueChange={setSelectedStatus} > - + - 전체 상태 - 정상 - 이상치 + 정상 + 이상치
- - + + +
- {isLoading && ( -
- 데이터를 불러오는 중... +
+
+ +
)} {!isLoading && (!vrfcTrgtSn || rowData.length === 0) && (
{vrfcTrgtSn ? "표시할 항목이 없습니다." - : "항목을 선택해주세요"} + : "항목을 선택해주세요."}
)} + {!isLoading && rowData.length > 0 && ( + + )}
); diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx index 62463b036..ddb6f88d3 100644 --- a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -1,7 +1,11 @@ import React, { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { + type ColumnDef, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api"; @@ -47,50 +51,47 @@ const VerificationList: React.FC = ({ return (detailData?.content ?? []) as StAnlsDtlResponse[]; }, [detailData]); - const columns = useMemo( + const columns: ColumnDef[] = useMemo( () => [ + { + header: "통계 년도", + accessorKey: "statsYr", + }, + { + header: "통계 항목", + accessorKey: "statsNm", + }, { header: "검증 항목", - accessorFn: (row: StAnlsDtlResponse) => - targetCodeMap.get(String(row.vrfcTrgtSn)) || - String(row.vrfcTrgtSn), - id: "vrfcTrgtNm", - size: 150, + accessorKey: "vrfcTrgtNm", }, { header: "μ", accessorKey: "ipcrCntVl", - size: 60, }, { header: "Percentile", accessorKey: "pctlCrtrVl", - size: 60, }, { header: "A", accessorKey: "scopAjmtCntVl", - size: 60, }, { header: "C", accessorKey: "thrsCntVl", - size: 60, }, { - header: "정상", + header: "정상건수", accessorKey: "nmlDataNocs", - size: 70, }, { - header: "제외", + header: "제외건수", accessorKey: "exclDataNocs", - size: 70, }, { - header: "이상", + header: "이상건수", accessorKey: "otlrDataNocs", - size: 70, }, ], [targetCodeMap], diff --git a/apps/st/src/entities/statistics/api/analysis-history-api.ts b/apps/st/src/entities/statistics/api/analysis-history-api.ts index 56375bf5f..5d6be6a4d 100644 --- a/apps/st/src/entities/statistics/api/analysis-history-api.ts +++ b/apps/st/src/entities/statistics/api/analysis-history-api.ts @@ -2,35 +2,34 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { - StAnlsMstListApiResponse, - StAnlsMstSearchParams, - StAnlsOutlierListResponse, + StAnlsMstListApiResponse, + StAnlsMstSearchParams, + StAnlsOutlierListResponse, } from "../model/analysis-history-interface"; export class AnalysisHistoryApiService { - public async getAnalysisHistoryList( - params: StAnlsMstSearchParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, - { - params, - }, - ); + public async getAnalysisHistoryList( + params: StAnlsMstSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`, + { + params, + }, + ); - return response.data; - } + return response.data; + } - public async getOutlierList( - anlsSn: number, - ): Promise { - const response = await client.get( - `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, - ); + public async getOutlierList( + anlsSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`, + ); - return response.data; - } + return response.data; + } } export const analysisHistoryApi = new AnalysisHistoryApiService(); - diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts index 7e5b95ba8..3ba448b16 100644 --- a/apps/st/src/entities/statistics/model/analysis-history-interface.ts +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -1,4 +1,4 @@ -import type { PageableList } from "@/shared/model"; +import type { PageableList, PageableParams } from "@/shared/model"; export interface StAnlsMstResponse { anlsSn: number; @@ -16,12 +16,9 @@ export interface StAnlsMstResponse { rgtrNm: string; } -export interface StAnlsMstSearchParams { +export interface StAnlsMstSearchParams extends PageableParams { anlsSttsCd?: string; jobUuid?: string; - page?: number; - size?: number; - sort?: string; verifyDtStart?: string; verifyDtEnd?: string; } diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts index 38c44b7a9..399be99e8 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -20,7 +20,7 @@ export interface StAnlsRsltResponse { chgRt: number | null; // 변경율 (r_i) trsfRt: number | null; // 변환 비율 (s_i) otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i) - otlrYn: string; // 이상치 여부 (y/n) + otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치) } export interface StAnlsRsltSearchParams extends PageableParams { From 8c384a51ce9178b86b69dd6dbc914553424b235a Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 11 Mar 2026 14:35:57 +0900 Subject: [PATCH 043/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/HbVerificationDetail.tsx | 22 +- .../verification/detail/Indicators.tsx | 9 +- .../verification/detail/RegionalGrid.tsx | 220 +++++++++++------- .../verification/detail/ScatterChart.tsx | 9 +- .../verification/detail/VerificationList.tsx | 5 + .../statistics-analysis-detail-interface.ts | 4 + 6 files changed, 162 insertions(+), 107 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 90ffd5350..359b25a67 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -62,20 +62,16 @@ const HbVerificationDetail: React.FC = ({ />
-
- -
+ -
- -
+
diff --git a/apps/st/src/components/statistics/verification/detail/Indicators.tsx b/apps/st/src/components/statistics/verification/detail/Indicators.tsx index eaabf7849..f311e1667 100644 --- a/apps/st/src/components/statistics/verification/detail/Indicators.tsx +++ b/apps/st/src/components/statistics/verification/detail/Indicators.tsx @@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import { LoadingWithAnimation } from "@common/components"; interface IndicatorsProps { anlsSn: number; @@ -34,8 +35,10 @@ const Indicators: React.FC = ({ anlsSn, vrfcTrgtSn }) => { if (isLoading) { return ( -
- 지표 데이터를 불러오는 중... +
+
+ +
); } @@ -61,7 +64,7 @@ const Indicators: React.FC = ({ anlsSn, vrfcTrgtSn }) => { ); return ( -
+
= ({ status: "", }); + const [gridRowData, setGridRowData] = useState< + Record[] + >([]); + const { data: regionCodes } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], queryFn: () => CodeApi.getCode("LAESECD"), @@ -99,34 +100,45 @@ const RegionalGrid: React.FC = ({ !appliedFilter.gugun && !appliedFilter.status ) { - return showAllOnEmptyFilter ? data : []; - } + data = showAllOnEmptyFilter ? data : []; + } else { + if (appliedFilter.sido) { + const sidoName = sidoOptions.find( + (opt) => opt.lwpoCd === appliedFilter.sido, + )?.lwpoCdNm; + if (sidoName) { + data = data.filter((item) => item.sido === sidoName); + } + } - if (appliedFilter.sido) { - const sidoName = sidoOptions.find( - (opt) => opt.lwpoCd === appliedFilter.sido, - )?.lwpoCdNm; - if (sidoName) { - data = data.filter((item) => item.sido === sidoName); + if (appliedFilter.gugun) { + const gugunName = gugunOptions.find( + (opt) => opt.lwpoCd === appliedFilter.gugun, + )?.lwpoCdNm; + if (gugunName) { + data = data.filter((item) => + gugunName.includes(item.gugun), + ); + } + } + + if (appliedFilter.status) { + data = data.filter( + (item) => + item.otlrYn.toLowerCase() === appliedFilter.status, + ); } } - if (appliedFilter.gugun) { - const gugunName = gugunOptions.find( - (opt) => opt.lwpoCd === appliedFilter.gugun, - )?.lwpoCdNm; - if (gugunName) { - data = data.filter((item) => gugunName.includes(item.gugun)); - } - } + const mappedData = data.map((item) => ({ + ...item, + otlrYn: item.otlrYn === "0" ? "정상" : "이상치", + })); - if (appliedFilter.status) { - data = data.filter( - (item) => item.otlrYn.toLowerCase() === appliedFilter.status, - ); - } - - return data; + setGridRowData( + mappedData as unknown as Record[], + ); + return mappedData; }, [ resultData, appliedFilter, @@ -135,60 +147,89 @@ const RegionalGrid: React.FC = ({ gugunOptions, ]); - const columns: ColumnDef[] = useMemo( - () => [ - { header: "시도", accessorKey: "sido", size: 80 }, - { header: "구군", accessorKey: "gugun", size: 80 }, - { header: "행정구역", accessorKey: "admdstNm", size: 100 }, - { header: "이전년도", accessorKey: "prvyrVl", size: 80 }, - { header: "당해년도", accessorKey: "cryrVl", size: 80 }, - { - header: "R", - accessorKey: "chgRt", - size: 60, - cell: (info) => Number(info.getValue() ?? 0).toFixed(2), - }, - { - header: "S", - accessorKey: "trsfRt", - size: 60, - cell: (info) => Number(info.getValue() ?? 0).toFixed(2), - }, - { - header: "E", - accessorKey: "otlrTrsfRt", - size: 60, - cell: (info) => Number(info.getValue() ?? 0).toFixed(2), - }, - { - header: "상태", - accessorKey: "otlrYn", - size: 60, - cell: (info) => { - const val = info.getValue(); - return ( - - {val === "0" ? "정상" : "이상치"} - - ); + const columnInfo: ColumnInfo = useMemo( + () => ({ + columns: [ + { + name: "sido", + fieldName: "sido", + header: { text: "시도" }, + width: 80, }, - }, - ], + { + name: "gugun", + fieldName: "gugun", + header: { text: "구군" }, + width: 80, + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "행정구역" }, + width: 100, + }, + { + name: "prvyrVl", + fieldName: "prvyrVl", + header: { text: "이전년도" }, + width: 80, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "cryrVl", + fieldName: "cryrVl", + header: { text: "당해년도" }, + width: 80, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "chgRt", + fieldName: "chgRt", + header: { text: "R" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "trsfRt", + fieldName: "trsfRt", + header: { text: "S" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "otlrTrsfRt", + fieldName: "otlrTrsfRt", + header: { text: "E" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "otlrYn", + fieldName: "otlrYn", + header: { text: "상태" }, + width: 60, + }, + ], + columnLayout: [ + "sido", + "gugun", + "admdstNm", + "prvyrVl", + "cryrVl", + "chgRt", + "trsfRt", + "otlrTrsfRt", + "otlrYn", + ], + }), [], ); - const table = useReactTable({ - data: rowData, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - initialState: { - pagination: { pageSize: 10 }, - }, - }); - const handleSidoChange = (val: string) => { onSidoSelect(!val ? null : val); setSelectedGugun(""); @@ -258,8 +299,8 @@ const RegionalGrid: React.FC = ({ - 정상 - 이상치 + 정상 + 이상치
@@ -269,7 +310,9 @@ const RegionalGrid: React.FC = ({ 초기화 - +
@@ -289,11 +332,12 @@ const RegionalGrid: React.FC = ({
)} {!isLoading && rowData.length > 0 && ( - )}
diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 97eef64c3..be23841b1 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -17,6 +17,7 @@ import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; +import { LoadingWithAnimation } from "@common/components"; ChartJS.register( LinearScale, @@ -231,11 +232,13 @@ const ScatterChart: React.FC = ({ 항목을 선택해주세요
) : isLoading ? ( -
- 차트를 불러오는 중... +
+
+ +
) : chartConfig ? ( -
+
= ({ const table = useReactTable({ data: rowData, columns, + state: { + rowSelection: selectedVrfcTrgtSn ? { [selectedVrfcTrgtSn]: true } : {}, + }, + enableMultiRowSelection: false, + getRowId: (row) => String(row.vrfcTrgtSn), getCoreRowModel: getCoreRowModel(), }); diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts index 3f6eb9003..af0919b87 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-detail-interface.ts @@ -18,6 +18,10 @@ export interface StAnlsDtlResponse { nmlDataNocs: number; // 정상 데이터 건수 exclDataNocs: number; // 제외 데이터 건수 otlrDataNocs: number; // 이상치 데이터 건수 + statsCd: string; // 통계 코드 + statsNm: string; // 통계 항목 명 + vrfcTrgtNm: string; // 검증대상명 + statsYr: string; // 통계년도 } export interface StAnlsDtlSearchParams extends PageableParams { From dabdf5739a3c081a887bf29f6406c12f367100f7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 11 Mar 2026 17:45:16 +0900 Subject: [PATCH 044/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=EC=9E=90=EB=A3=8C=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EB=B0=8F=20QA=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 3 +- .../VerificationArticleManagement.tsx | 26 ++++++----- .../detail/HbVerificationDetail.tsx | 19 +++++++- .../verification/detail/RegionalGrid.tsx | 46 ++++++++++++++++++- .../verification/detail/ScatterChart.tsx | 8 ++-- .../verification/detail/VerificationList.tsx | 28 ++++++----- .../api/statistics-analysis-result-api.ts | 14 ++++++ .../model/analysis-history-interface.ts | 1 + .../model/verification-article-column.ts | 12 ++++- 9 files changed, 126 insertions(+), 31 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 4da13d0e2..831fa6057 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -180,7 +180,8 @@ const HbStatisticsVerification = () => { const params: StAnlsMstSearchParams = { page: historyPagination.pageIndex, size: historyPagination.pageSize, - sort: "anlsSn,desc", + sort: "anlsSn,desc", + vrfcMthdCd: "HB", }; if (appliedHistoryDateRange?.from) { diff --git a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx index caef0180b..b852b1496 100644 --- a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx +++ b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx @@ -1,11 +1,22 @@ "use client"; -import DataGrid from "@/components/module/DataGrid"; +import { useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; +import { CodeSelect } from "@/entities/code/ui/code-select"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import { hbArticleColumnInfo, imrArticleColumnInfo, validationArticleColumnInfo, } from "@/entities/statistics/model/verification-article-column"; +import type { + VerificationTargetManagementResponse, + VerificationTargetManagementSearchParams, +} from "@/entities/statistics/model/verification-target-management-interface"; import { Button, Input, @@ -15,17 +26,10 @@ import { TabsList, TabsTrigger, } from "@common/components"; -import { CodeSelect } from "@/entities/code/ui/code-select"; -import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { ENDPOINT } from "@/shared/config"; -import type { - VerificationTargetManagementSearchParams, - VerificationTargetManagementResponse, -} from "@/entities/statistics/model/verification-target-management-interface"; + import { VerificationTargetDialog } from "./VerificationTargetDialog"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import DataGrid from "@/components/module/DataGrid"; const TAB_CONFIG = { HB: { value: "HB", label: "H-B", columnInfo: hbArticleColumnInfo }, diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 359b25a67..71f4f28c8 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; +import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface"; import { Button } from "@common/components"; import Indicators from "./Indicators"; @@ -20,6 +21,18 @@ const HbVerificationDetail: React.FC = ({ null, ); const [selectedSido, setSelectedSido] = useState(null); + const [selectedTargetInfo, setSelectedTargetInfo] = useState<{ + statsNm: string; + vrfcTrgtNm: string; + } | null>(null); + + const handleTargetSelect = (row: StAnlsDtlResponse) => { + setSelectedVrfcTrgtSn(String(row.vrfcTrgtSn)); + setSelectedTargetInfo({ + statsNm: row.statsNm, + vrfcTrgtNm: row.vrfcTrgtNm, + }); + }; return (
@@ -44,7 +57,7 @@ const HbVerificationDetail: React.FC = ({
@@ -57,6 +70,10 @@ const HbVerificationDetail: React.FC = ({ diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index d1dafcd88..7b8d16a5d 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -23,6 +23,8 @@ import type { ColumnInfo } from "@/components/type/DataGrid.type"; interface RegionalGridProps { anlsSn: number; vrfcTrgtSn: string | null; + statsNm: string; + vrfcTrgtNm: string; selectedSido: string | null; onSidoSelect: (sido: string | null) => void; } @@ -36,6 +38,8 @@ interface AppliedFilter { const RegionalGrid: React.FC = ({ anlsSn, vrfcTrgtSn, + statsNm, + vrfcTrgtNm, selectedSido, onSidoSelect, }) => { @@ -48,6 +52,7 @@ const RegionalGrid: React.FC = ({ gugun: "", status: "", }); + const [isDownloading, setIsDownloading] = useState(false); const [gridRowData, setGridRowData] = useState< Record[] @@ -256,6 +261,31 @@ const RegionalGrid: React.FC = ({ setShowAllOnEmptyFilter(true); }; + const handleDownload = async () => { + if (!vrfcTrgtSn) return; + setIsDownloading(true); + + try { + const blob = await stAnlsRsltApi.downloadExcel( + anlsSn, + Number(vrfcTrgtSn), + ); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `분석결과_${statsNm}-${vrfcTrgtNm}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error("Failed to download excel", error); + } finally { + setIsDownloading(false); + } + }; + return (
@@ -310,8 +340,20 @@ const RegionalGrid: React.FC = ({ 초기화 -
diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index be23841b1..2da038fcd 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -6,6 +6,7 @@ import { Chart as ChartJS, Legend, LinearScale, + LineController, LineElement, PointElement, ScatterController, @@ -21,11 +22,12 @@ import { LoadingWithAnimation } from "@common/components"; ChartJS.register( LinearScale, - PointElement, + LineController, LineElement, - Tooltip, - Legend, + PointElement, ScatterController, + Legend, + Tooltip, zoomPlugin, ); diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx index 9d28b7164..5c2f89065 100644 --- a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -16,7 +16,7 @@ import { DataTable } from "@common/components"; interface VerificationListProps { anlsSn: number; selectedVrfcTrgtSn: string | null; - onSelect: (vrfcTrgtSn: string) => void; + onSelect: (row: StAnlsDtlResponse) => void; } const VerificationList: React.FC = ({ @@ -101,7 +101,9 @@ const VerificationList: React.FC = ({ data: rowData, columns, state: { - rowSelection: selectedVrfcTrgtSn ? { [selectedVrfcTrgtSn]: true } : {}, + rowSelection: selectedVrfcTrgtSn + ? { [selectedVrfcTrgtSn]: true } + : {}, }, enableMultiRowSelection: false, getRowId: (row) => String(row.vrfcTrgtSn), @@ -109,13 +111,13 @@ const VerificationList: React.FC = ({ }); const handleRowClick = (row: StAnlsDtlResponse) => { - onSelect(String(row.vrfcTrgtSn)); + onSelect(row); }; useEffect(() => { if (!selectedVrfcTrgtSn && rowData.length > 0) { - const firstTargetSn = rowData[0]?.vrfcTrgtSn; - if (firstTargetSn) onSelect(String(firstTargetSn)); + const firstRow = rowData[0]; + if (firstRow) onSelect(firstRow); } }, [selectedVrfcTrgtSn, rowData, onSelect]); @@ -132,13 +134,15 @@ const VerificationList: React.FC = ({
)} {!isLoading && rowData.length > 0 && ( - +
+ +
)}
); diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts index 604e4144a..e8e40f0d2 100644 --- a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts +++ b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts @@ -31,6 +31,20 @@ export class StAnlsRsltApiService { return response.data; } + + public async downloadExcel( + anlsSn: number, + vrfcTrgtSn: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/excel-down/${anlsSn}/${vrfcTrgtSn}`, + { + responseType: "blob", + }, + ); + + return response.data; + } } export const stAnlsRsltApi = new StAnlsRsltApiService(); diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts index 3ba448b16..ee31b936d 100644 --- a/apps/st/src/entities/statistics/model/analysis-history-interface.ts +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -21,6 +21,7 @@ export interface StAnlsMstSearchParams extends PageableParams { jobUuid?: string; verifyDtStart?: string; verifyDtEnd?: string; + vrfcMthdCd?: string; } export interface StAnlsOutlierDataResponse { diff --git a/apps/st/src/entities/statistics/model/verification-article-column.ts b/apps/st/src/entities/statistics/model/verification-article-column.ts index c62156730..e9e237338 100644 --- a/apps/st/src/entities/statistics/model/verification-article-column.ts +++ b/apps/st/src/entities/statistics/model/verification-article-column.ts @@ -1,5 +1,5 @@ -import { ValueType } from "realgrid"; import type { ColumnInfo } from "@rsone/components"; +import { ValueType } from "realgrid"; export const hbArticleColumnInfo: ColumnInfo = { columns: [ @@ -31,6 +31,15 @@ export const hbArticleColumnInfo: ColumnInfo = { editable: false, sortdisable: false, }, + { + name: "ipcrCntVl", + fieldName: "ipcrCntVl", + header: { text: "μ" }, + editable: false, + sortdisable: false, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.##", + }, { name: "pctlCrtrVl", fieldName: "pctlCrtrVl", @@ -78,6 +87,7 @@ export const hbArticleColumnInfo: ColumnInfo = { "vrfcTrgtNm", "tblNm", "colNm", + "ipcrCntVl", "pctlCrtrVl", "scopCntCntVl", "thrsCntVl", From d731a3fc5b2465dd23ce58533acea0832f15fef5 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 12 Mar 2026 10:42:07 +0900 Subject: [PATCH 045/113] =?UTF-8?q?idea=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e4fab5cdd..fd688dd4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ # dependencies node_modules/ - +.idea/ # build .next/ dist/ From bf577ddc81008110b0d8c8058002b765dfd01ba3 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 12 Mar 2026 10:51:29 +0900 Subject: [PATCH 046/113] =?UTF-8?q?idea=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/cs/.gitignore b/apps/cs/.gitignore index b63718b61..f80a96304 100644 --- a/apps/cs/.gitignore +++ b/apps/cs/.gitignore @@ -38,4 +38,5 @@ yarn-error.log* next-env.d.ts .env* -!.env.example \ No newline at end of file +!.env.example +/start_cs.log From 969d24736d30b7b6cd522eff5d1b5c8535e78e29 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 14:59:34 +0900 Subject: [PATCH 047/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/detail/RegionalGrid.tsx | 21 +-- .../verification/detail/ScatterChart.tsx | 136 ++++++++---------- .../st/src/entities/statistics/api/queries.ts | 43 ++++++ .../statistics-analysis-result-interface.ts | 6 +- 4 files changed, 110 insertions(+), 96 deletions(-) create mode 100644 apps/st/src/entities/statistics/api/queries.ts diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 7b8d16a5d..72f344291 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -5,6 +5,7 @@ import { ValueType } from "realgrid"; import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; +import { useStAnlsRsltList } from "@/entities/statistics/api/queries"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { @@ -80,22 +81,10 @@ const RegionalGrid: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { data: resultData, isLoading } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "list", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => - stAnlsRsltApi.getResultList({ - anlsSn, - vrfcTrgtSn: Number(vrfcTrgtSn), - page: 0, - size: 1000, - }), - enabled: !!vrfcTrgtSn, - }); + const { data: resultData, isLoading } = useStAnlsRsltList( + anlsSn, + vrfcTrgtSn, + ); const rowData = useMemo(() => { let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 2da038fcd..1b2fb5127 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -4,6 +4,8 @@ import { Scatter } from "react-chartjs-2"; import { useQuery } from "@tanstack/react-query"; import { Chart as ChartJS, + type ChartData, + type ChartOptions, Legend, LinearScale, LineController, @@ -11,12 +13,16 @@ import { PointElement, ScatterController, Tooltip, + type TooltipItem, } from "chart.js"; import zoomPlugin from "chartjs-plugin-zoom"; import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; -import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import { + useStAnlsChartData, + useStAnlsRsltList, +} from "@/entities/statistics/api/queries"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { LoadingWithAnimation } from "@common/components"; @@ -37,6 +43,15 @@ interface ScatterChartProps { selectedSido: string | null; } +interface LinePoint { + x: number; + y: number; +} + +interface DataPoint extends LinePoint { + item: StAnlsRsltResponse; +} + const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, @@ -54,110 +69,79 @@ const ScatterChart: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { data: resultData, isLoading: isLoadingGrid } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "list", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => - stAnlsRsltApi.getResultList({ - anlsSn, - vrfcTrgtSn: Number(vrfcTrgtSn), - page: 0, - size: 1000, - }), - enabled: !!anlsSn && !!vrfcTrgtSn, - }); - - const { data: chartData, isLoading: isLoadingChart } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "chart", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), - enabled: !!anlsSn && !!vrfcTrgtSn, - }); + const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList( + anlsSn, + vrfcTrgtSn, + ); + const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData( + anlsSn, + vrfcTrgtSn, + ); const isLoading = isLoadingGrid || isLoadingChart; const chartConfig = useMemo(() => { if (!resultData || !chartData) return null; - const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[]; + const rawPoints = resultData.content ?? []; const boundaries = chartData.list ?? []; const sortedBoundaries = [...boundaries].sort( - (a, b) => a.xPrev - b.xPrev, + (a, b) => a.xprev - b.xprev, ); - const upperLineData = sortedBoundaries.map((b) => ({ - x: b.xPrev, - y: b.yUpper, - })); - const lowerLineData = sortedBoundaries.map((b) => ({ - x: b.xPrev, - y: b.yLower, + + const upperLineData: LinePoint[] = sortedBoundaries.map((b) => ({ + x: b.yupper, + y: b.xprev, })); - const normalPoints: { - x: number; - y: number; - item: StAnlsRsltResponse; - }[] = []; - const outlierPoints: { - x: number; - y: number; - item: StAnlsRsltResponse; - }[] = []; + const lowerLineData: LinePoint[] = sortedBoundaries.map((b) => ({ + x: b.xprev, + y: b.ylower, + })); + + const normalPoints: DataPoint[] = []; + const outlierPoints: DataPoint[] = []; rawPoints.forEach((item) => { if (selectedSidoName && item.sido !== selectedSidoName) { return; } - const pt = { + const pt: DataPoint = { x: Number(item.prvyrVl ?? 0), y: Number(item.cryrVl ?? 0), item, }; - if (item.otlrYn.toLowerCase() === "y") { + if (item.otlrYn === "0") { normalPoints.push(pt); } else { outlierPoints.push(pt); } }); - const data = { + const data: ChartData<"scatter"> = { datasets: [ { label: "상한선", data: upperLineData, - type: "line" as const, + showLine: true, borderColor: "rgba(255, 99, 132, 0.8)", - borderWidth: 2, - fill: false, pointRadius: 0, - tension: 0.1, }, { label: "하한선", data: lowerLineData, - type: "line" as const, + showLine: true, borderColor: "rgba(54, 162, 235, 0.8)", - borderWidth: 2, - fill: false, pointRadius: 0, - tension: 0.1, }, { label: "이상치", data: outlierPoints, backgroundColor: "rgba(255, 99, 132, 0.7)", - pointRadius: 5, + pointRadius: 4, pointHoverRadius: 8, }, { @@ -165,19 +149,19 @@ const ScatterChart: React.FC = ({ data: normalPoints, backgroundColor: "rgba(75, 192, 192, 0.7)", pointRadius: 4, - pointHoverRadius: 7, + pointHoverRadius: 8, }, ], }; - const options = { + const options: ChartOptions<"scatter"> = { responsive: true, maintainAspectRatio: false, plugins: { zoom: { pan: { enabled: true, - mode: "xy" as const, + mode: "xy", }, zoom: { wheel: { @@ -186,36 +170,34 @@ const ScatterChart: React.FC = ({ pinch: { enabled: true, }, - mode: "xy" as const, + mode: "xy", }, }, tooltip: { callbacks: { - label: (context: any) => { - if (context.dataset.type === "line") { - return `${context.dataset.label}: (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`; + label: (context: TooltipItem<"scatter">) => { + const dataset = context.dataset; + if (dataset.showLine) { + return `${context.dataset.label}: (${context?.parsed?.x?.toFixed(2) ?? "-"}, ${context?.parsed?.y?.toFixed(2) ?? "-"})`; } - const pt = context.raw as { - x: number; - y: number; - item: StAnlsRsltResponse; - }; - return `${pt.item.sido} ${pt.item.gugun}: 이전(${pt.x}), 당해(${pt.y})`; + + const pt = (context.raw as DataPoint) ?? {}; + return `${pt?.item?.sido ?? "시/도"} ${pt?.item?.gugun ?? "구/군"}: 이전(${pt?.x ?? "-"}), 당해(${pt?.y ?? "-"})`; }, }, }, }, scales: { x: { - type: "linear" as const, - position: "bottom" as const, + type: "linear", + position: "bottom", title: { display: true, text: "이전년도 값", }, }, y: { - type: "linear" as const, + type: "linear", title: { display: true, text: "당해년도 값", @@ -242,8 +224,8 @@ const ScatterChart: React.FC = ({ ) : chartConfig ? (
) : ( diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts new file mode 100644 index 000000000..bd8510421 --- /dev/null +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -0,0 +1,43 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import { stAnlsRsltApi } from "./statistics-analysis-result-api"; + +export const useStAnlsRsltList = ( + anlsSn: number, + vrfcTrgtSn: string | null, +) => { + return useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "list", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => + stAnlsRsltApi.getResultList({ + anlsSn, + vrfcTrgtSn: Number(vrfcTrgtSn), + page: 0, + size: 1000, + }), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); +}; + +export const useStAnlsChartData = ( + anlsSn: number, + vrfcTrgtSn: string | null, +) => { + return useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "chart", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); +}; diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts index 399be99e8..a2a3e5e84 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -35,9 +35,9 @@ export interface StAnlsRsltSearchParams extends PageableParams { export type StAnlsRsltListApiResponse = PageableList; export interface ChartBoundaryPointDto { - xPrev: number; // 전년도 값 (x축) - yLower: number; // 이상치 하한 당년값 (y축) - yUpper: number; // 이상치 상한 당년값 (y축) + xprev: number; // 전년도 값 (x축) + ylower: number; // 이상치 하한 당년값 (y축) + yupper: number; // 이상치 상한 당년값 (y축) } export interface ChartRetDto { From b6e11b4be41c08a0392f0685cb342756262ac9f7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 15:20:42 +0900 Subject: [PATCH 048/113] =?UTF-8?q?[st]=20feat:=20=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=84=B0=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8B=B1?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=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/DataGrid.tsx | 362 ++++++++++++++---- .../detail/HbVerificationDetail.tsx | 4 + .../verification/detail/RegionalGrid.tsx | 4 + .../verification/detail/ScatterChart.tsx | 23 ++ apps/st/src/components/type/DataGrid.type.ts | 4 + 5 files changed, 313 insertions(+), 84 deletions(-) diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index a3f2be7c1..f914be8a8 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1,24 +1,25 @@ -import { useRef, useEffect, useLayoutEffect } from "react"; -import { ValueType } from "realgrid"; +import { useEffect, useLayoutEffect, useRef } from "react"; -import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid"; -import { RealGridReact, RGDataColumn } from "realgrid-react"; -import type { - DataGridProps, - RealGridColumnInfo, - GridViewWithSetColumns, - FooterItemWithCallback, - ColumnLayoutItem, - CellStyleCallback, - CellTooltipCallback, -} from "../type/DataGrid.type"; -import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import JSZip from "jszip"; +import { ValueType } from "realgrid"; +import RealGrid, { type DisplayOptions, GridFitStyle } from "realgrid"; +import { RealGridReact, RGDataColumn } from "realgrid-react"; import * as XLSX from "xlsx"; import { useAlertStore } from "@/shared/lib"; - import { Button } from "@common/components"; + +import type { + CellStyleCallback, + CellTooltipCallback, + ColumnLayoutItem, + DataGridProps, + FooterItemWithCallback, + GridViewWithSetColumns, + RealGridColumnInfo, +} from "../type/DataGrid.type"; + +import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { useTabStore } from "@/store/tab-store"; // RealGrid 타입 확장 @@ -321,19 +322,43 @@ const DataGrid = ({ cellTooltipCallback, onRowClick, statusInfo, + highlightedRowId, + idFieldName = "id", }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); const fileInputRef = useRef(null); const sortDirRef = useRef>({}); - const originalRowsRef = useRef[]>>({}); + const originalRowsRef = useRef< + Record[]> + >({}); const rowDataRef = useRef[]>(rowData); - const cellStyleCallbackRef = useRef(cellStyleCallback); - const cellTooltipCallbackRef = useRef(cellTooltipCallback); + const cellStyleCallbackRef = useRef( + cellStyleCallback, + ); + const cellTooltipCallbackRef = useRef( + cellTooltipCallback, + ); rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; cellTooltipCallbackRef.current = cellTooltipCallback; + // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 + useEffect(() => { + if (!gridRef.current?.gridView || highlightedRowId == null) return; + + const gridView = gridRef.current.gridView; + const index = rowData.findIndex( + (row) => String(row[idFieldName]) === String(highlightedRowId), + ); + + if (index !== -1) { + // 해당 행으로 포커스 이동 및 스크롤 + gridView.setCurrent({ itemIndex: index }); + gridView.setFocus(); + } + }, [highlightedRowId, rowData, idFieldName]); + useLayoutEffect(() => { if (!cellStyleCallback || !gridRef.current?.gridView) return; gridRef.current.gridView.refresh(); @@ -532,10 +557,10 @@ const DataGrid = ({ ) { const cell = worksheet[ - XLSX.utils.encode_cell({ - r: rowIndex, - c: col, - }) + XLSX.utils.encode_cell({ + r: rowIndex, + c: col, + }) ]; if ( cell && @@ -611,7 +636,7 @@ const DataGrid = ({ const col = range.s.c + colIndex; const cell = worksheet[ - XLSX.utils.encode_cell({ r: row, c: col }) + XLSX.utils.encode_cell({ r: row, c: col }) ]; const headerName = headers[colIndex] || `Column${colIndex + 1}`; @@ -909,7 +934,7 @@ const DataGrid = ({ ) { ( column.footer[ - index + index ] as FooterItemWithCallback ).valueCallback = footerItem.valueCallback; @@ -962,11 +987,11 @@ const DataGrid = ({ getGridDataRef.current = () => { const gv = gridRef.current?.gridView as | { - getDataSource?: () => { - getRowCount(): number; - getValue(r: number, f: string): unknown; - }; - } + getDataSource?: () => { + getRowCount(): number; + getValue(r: number, f: string): unknown; + }; + } | undefined; if (!gv?.getDataSource) return []; const ds = gv.getDataSource(); @@ -1001,23 +1026,57 @@ const DataGrid = ({ if (!gridView || (!onRowClick && !cellTooltipCallback)) return; gridView.onCellClicked = (grid, clickData) => { - if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return; + if ( + clickData.cellType !== "data" || + clickData.itemIndex === undefined + ) + return; try { const row = rowDataRef.current[clickData.itemIndex]; - const cellIndex = (grid as { getCurrent?: () => { itemIndex: number; column?: string; fieldName?: string } }).getCurrent?.(); + const cellIndex = ( + grid as { + getCurrent?: () => { + itemIndex: number; + column?: string; + fieldName?: string; + }; + } + ).getCurrent?.(); // cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex) if (cellTooltipCallbackRef.current && row && cellIndex) { - const fieldName = cellIndex.fieldName ?? cellIndex.column ?? ""; - const msg = cellTooltipCallbackRef.current({ row: row as Record, fieldName }); + const fieldName = + cellIndex.fieldName ?? cellIndex.column ?? ""; + const msg = cellTooltipCallbackRef.current({ + row: row as Record, + fieldName, + }); if (msg) { - const colName = columns.find((c) => c.fieldName === fieldName || c.name === cellIndex.column)?.name ?? cellIndex.column ?? fieldName; - (gridView as { showMessage?: (opts: { message: string; hideDuration?: number }, idx: { itemIndex: number; column: string }) => void }).showMessage?.( + const colName = + columns.find( + (c) => + c.fieldName === fieldName || + c.name === cellIndex.column, + )?.name ?? + cellIndex.column ?? + fieldName; + ( + gridView as { + showMessage?: ( + opts: { + message: string; + hideDuration?: number; + }, + idx: { itemIndex: number; column: string }, + ) => void; + } + ).showMessage?.( { message: msg, hideDuration: 5000 }, - { itemIndex: cellIndex.itemIndex, column: colName } + { itemIndex: cellIndex.itemIndex, column: colName }, ); } } - if (onRowClick) onRowClick((row ?? {}) as Record); + if (onRowClick) + onRowClick((row ?? {}) as Record); } catch (error) { console.error("행 데이터 가져오기 오류:", error); } @@ -1038,26 +1097,42 @@ const DataGrid = ({ onChange={handleFileChange} />
- {statusInfo && + {statusInfo && (
- 진행상황: { - statusInfo.laeSbmsnSttsCd === null || statusInfo.laeSbmsnSttsCd === '0' ? - '미작성' - : - statusInfo.laeSbmsnSttsCd === '1' ? '작성중' : statusInfo.laeSbmsnSttsCd === '2' ? '제출' - : - statusInfo.laeSbmsnSttsCd === '3' ? '승인' : '반려' - } + + 진행상황:{" "} + {statusInfo.laeSbmsnSttsCd === null || + statusInfo.laeSbmsnSttsCd === "0" + ? "미작성" + : statusInfo.laeSbmsnSttsCd === "1" + ? "작성중" + : statusInfo.laeSbmsnSttsCd === "2" + ? "제출" + : statusInfo.laeSbmsnSttsCd === "3" + ? "승인" + : "반려"} + + + 제출일:{" "} + {statusInfo.laeSbmsnYmd + ? statusInfo.laeSbmsnYmd.length === 8 && + /^\d{8}$/.test(statusInfo.laeSbmsnYmd) + ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` + : statusInfo.laeSbmsnYmd + : "미제출"} + + + 담당자:{" "} + {statusInfo.laePicNm ? statusInfo.laePicNm : "-"} + + + 연락처:{" "} + {statusInfo.laePicTelNo + ? statusInfo.laePicTelNo + : "-"} - 제출일: {statusInfo.laeSbmsnYmd - ? (statusInfo.laeSbmsnYmd.length === 8 && /^\d{8}$/.test(statusInfo.laeSbmsnYmd) - ? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}` - : statusInfo.laeSbmsnYmd) - : '미제출'} - 담당자: {statusInfo.laePicNm ? statusInfo.laePicNm : '-'} - 연락처: {statusInfo.laePicTelNo ? statusInfo.laePicTelNo : '-'}
- } + )} {isExcelImport && (
@@ -88,6 +91,7 @@ const HbVerificationDetail: React.FC = ({ anlsSn={anlsSn} vrfcTrgtSn={selectedVrfcTrgtSn} selectedSido={selectedSido} + onPointClick={setSelectedRowId} />
diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 72f344291..7b2b42a07 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -28,6 +28,7 @@ interface RegionalGridProps { vrfcTrgtNm: string; selectedSido: string | null; onSidoSelect: (sido: string | null) => void; + selectedRowId?: number | null; } interface AppliedFilter { @@ -43,6 +44,7 @@ const RegionalGrid: React.FC = ({ vrfcTrgtNm, selectedSido, onSidoSelect, + selectedRowId, }) => { const [selectedGugun, setSelectedGugun] = useState(""); const [selectedStatus, setSelectedStatus] = useState(""); @@ -369,6 +371,8 @@ const RegionalGrid: React.FC = ({ setRowData={setGridRowData} isExcelExport={false} isExcelImport={false} + highlightedRowId={selectedRowId} + idFieldName="rsltSn" /> )}
diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 1b2fb5127..46aab674a 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -41,6 +41,7 @@ interface ScatterChartProps { anlsSn: number; vrfcTrgtSn: string | null; selectedSido: string | null; + onPointClick?: (rsltSn: number | null) => void; } interface LinePoint { @@ -56,6 +57,7 @@ const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, selectedSido, + onPointClick, }) => { const { data: regionCodes } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], @@ -157,6 +159,27 @@ const ScatterChart: React.FC = ({ const options: ChartOptions<"scatter"> = { responsive: true, maintainAspectRatio: false, + onClick: (event, elements) => { + if (elements.length > 0) { + const element = elements[0]; + if (!element) return; + const datasetIndex = element.datasetIndex; + const index = element.index; + const dataset = data.datasets[datasetIndex]; + + if (!dataset) return; + + // 상한선/하한선 클릭 시 무시 + if (dataset.showLine) return; + + const pt = dataset.data[index] as DataPoint; + if (pt?.item?.rsltSn) { + onPointClick?.(pt.item.rsltSn); + } + } else { + onPointClick?.(null); + } + }, plugins: { zoom: { pan: { diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 1423c7940..18523b0c4 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -244,6 +244,10 @@ export type DataGridProps = { }; onRowClick?: (rowData: Record) => void; // 행 클릭 이벤트 statusInfo?: SewerageSupplyRateStatusInfo | null; + /** 강조 및 스크롤할 행의 ID */ + highlightedRowId?: string | number | null; + /** ID로 사용할 필드명 (기본값: 'id') */ + idFieldName?: string; }; export type TabDataGridProps = { From 9d6902f521a9947729287453bdc940cb7da1f03a Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 15:38:55 +0900 Subject: [PATCH 049/113] =?UTF-8?q?[st]=20fix:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=83=81=EC=84=B8=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/HbVerificationDetail.tsx | 1 - .../verification/detail/RegionalGrid.tsx | 17 +++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 64c2f76b8..026ed7688 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -76,7 +76,6 @@ const HbVerificationDetail: React.FC = ({ vrfcTrgtNm={ selectedTargetInfo?.vrfcTrgtNm || "" } - selectedSido={selectedSido} onSidoSelect={setSelectedSido} selectedRowId={selectedRowId} /> diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 7b2b42a07..abfa0fdfb 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ValueType } from "realgrid"; @@ -26,7 +26,6 @@ interface RegionalGridProps { vrfcTrgtSn: string | null; statsNm: string; vrfcTrgtNm: string; - selectedSido: string | null; onSidoSelect: (sido: string | null) => void; selectedRowId?: number | null; } @@ -42,16 +41,16 @@ const RegionalGrid: React.FC = ({ vrfcTrgtSn, statsNm, vrfcTrgtNm, - selectedSido, onSidoSelect, selectedRowId, }) => { + const [selectedSido, setSelectedSido] = useState(""); const [selectedGugun, setSelectedGugun] = useState(""); const [selectedStatus, setSelectedStatus] = useState(""); const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] = useState(true); const [appliedFilter, setAppliedFilter] = useState({ - sido: null, + sido: "", gugun: "", status: "", }); @@ -227,11 +226,12 @@ const RegionalGrid: React.FC = ({ ); const handleSidoChange = (val: string) => { - onSidoSelect(!val ? null : val); + setSelectedSido(val); setSelectedGugun(""); }; const handleSearch = () => { + onSidoSelect(selectedSido); setAppliedFilter({ sido: selectedSido, gugun: selectedGugun, @@ -242,10 +242,11 @@ const RegionalGrid: React.FC = ({ const handleReset = () => { onSidoSelect(null); + setSelectedSido(""); setSelectedGugun(""); setSelectedStatus(""); setAppliedFilter({ - sido: null, + sido: "", gugun: "", status: "", }); @@ -277,6 +278,10 @@ const RegionalGrid: React.FC = ({ } }; + useEffect(() => { + handleReset(); + }, [vrfcTrgtSn]); + return (
From e610b3b8e81e37fc2423dbc9c9f7b8e4c93cf998 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 16:10:22 +0900 Subject: [PATCH 050/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EA=B7=B8=EB=A6=AC=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=88=B4=ED=8C=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 3 +- .../model/analysis-history-interface.ts | 92 +++++++++++++++++-- .../model/verification-history-columns.tsx | 30 +++++- 3 files changed, 112 insertions(+), 13 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 831fa6057..99a4a6e0b 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -208,8 +208,7 @@ const HbStatisticsVerification = () => { }, }); - const historyRowData: StAnlsMstResponse[] = (analysisHistory?.content ?? - []) as StAnlsMstResponse[]; + const historyRowData: StAnlsMstResponse[] = analysisHistory?.content ?? []; const table = useReactTable({ data: historyRowData, diff --git a/apps/st/src/entities/statistics/model/analysis-history-interface.ts b/apps/st/src/entities/statistics/model/analysis-history-interface.ts index ee31b936d..a680dcc77 100644 --- a/apps/st/src/entities/statistics/model/analysis-history-interface.ts +++ b/apps/st/src/entities/statistics/model/analysis-history-interface.ts @@ -1,49 +1,123 @@ import type { PageableList, PageableParams } from "@/shared/model"; -export interface StAnlsMstResponse { +/** + * 분석 현황 마스터 응답 DTO + */ +export interface StAnlsMstResponseDto { + /** 분석번호 */ anlsSn: number; + /** 분석실행일시 (yyyy-MM-dd HH:mm:ss) */ anlsBgngDt: string; + /** 분석종료일시 (yyyy-MM-dd HH:mm:ss) */ anlsEndDt: string | null; + /** 분석상태코드 */ anlsSttsCd: string; + /** 분석상태코드명 */ anlsSttsNm: string; - anlsPrgrsRt?: number; + /** 정상건수 */ nmlDataNocs: number; + /** 제외건수 */ exclDataNocs: number; + /** 이상건수 */ otlrDataNocs: number; + /** 작업UUID */ jobUuid: string; + /** 등록일시 (yyyy-MM-dd HH:mm:ss) */ regDt: string; + /** 등록자 */ rgtr: string; + /** 등록자명 */ rgtrNm: string; + /** 오류메세지내용 */ + errMsgCn?: string; + /** 통계년도 */ + statsYr: string; + /** 검증대상명 */ + vrfcTrgtNm: string; + /** 분석진행비율 */ + anlsPrgrsRt: number; + /** 하수도사업자코드 */ + sewerBzmnCd: string; + /** 통계코드 */ + statsCd: string; + /** 검증방법코드 */ + vrfcMthdCd: string; } -export interface StAnlsMstSearchParams extends PageableParams { +/** + * 분석 현황 마스터 검색 DTO + */ +export interface StAnlsMstSearchDto extends PageableParams { + /** 분석상태코드 (COMPLETED, FAILED, QUEUED, PROCESSING) */ anlsSttsCd?: string; + /** 작업UUID */ jobUuid?: string; - verifyDtStart?: string; - verifyDtEnd?: string; + /** 통계년도 */ + statsYr?: string; + /** 검증대상명 */ + vrfcTrgtNm?: string; + /** 하수도사업자코드 */ + sewerBzmnCd?: string; + /** 통계코드 */ + statsCd?: string; + /** 검증방법코드 */ vrfcMthdCd?: string; + /** 검증일시 시작 (yyyy-MM-dd) */ + verifyDtStart?: string; + /** 검증일시 종료 (yyyy-MM-dd) */ + verifyDtEnd?: string; } -export interface StAnlsOutlierDataResponse { +/** + * 분석 이상치 상세 데이터 응답 DTO + */ +export interface StAnlsOutlierDataResponseDto { + /** 결과 일련번호 */ rsltSn: number; + /** 검증 대상 일련번호 */ vrfcTrgtSn: number; + /** 행정구역 코드 */ admdstCd: string; + /** 시도 */ sido: string; + /** 시군구 */ gugun: string; + /** 행정동 */ adstDong: string; + /** 시설명 */ + fcltNm: string; + /** 주소 */ addr: string; + /** 당해년도 값 */ cryrVl: number; + /** 이상치 변환 비율 */ otlrTrsfRt: number; } -export interface StAnlsOutlierListResponse { +/** + * 분석 순번별 이상치 분석항목 응답 DTO + */ +export interface StAnlsOutlierListResponseDto { + /** 분석번호 */ anlsSn: number; + /** 검증 대상 일련번호 */ vrfcTrgtSn: number; + /** 검증대상명 */ vrfcTrgtNm: string; + /** 정상건수 */ nmlDataNocs: number; + /** 제외건수 */ exclDataNocs: number; + /** 이상건수 */ otlrDataNocs: number; - outlierDataList: StAnlsOutlierDataResponse[]; + /** 이상치 상세 데이터 목록 */ + outlierDataList: StAnlsOutlierDataResponseDto[]; } -export type StAnlsMstListApiResponse = PageableList; +/** 기존 인터페이스 호환을 위한 별칭 (필요 시 유지) */ +export type StAnlsMstResponse = StAnlsMstResponseDto; +export type StAnlsMstSearchParams = StAnlsMstSearchDto; +export type StAnlsOutlierDataResponse = StAnlsOutlierDataResponseDto; +export type StAnlsOutlierListResponse = StAnlsOutlierListResponseDto; + +export type StAnlsMstListApiResponse = PageableList; diff --git a/apps/st/src/entities/statistics/model/verification-history-columns.tsx b/apps/st/src/entities/statistics/model/verification-history-columns.tsx index 766709217..f5fc29b38 100644 --- a/apps/st/src/entities/statistics/model/verification-history-columns.tsx +++ b/apps/st/src/entities/statistics/model/verification-history-columns.tsx @@ -1,5 +1,12 @@ import { createColumnHelper } from "@tanstack/react-table"; -import { Progress } from "@common/components"; + +import { + Progress, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@common/components"; + import type { StAnlsMstResponse } from "./analysis-history-interface"; const columnHelper = createColumnHelper(); @@ -16,12 +23,14 @@ export const verificationHistoryColumns = [ anlsSttsCd: row.anlsSttsCd, anlsSttsNm: row.anlsSttsNm, anlsPrgrsRt: row.anlsPrgrsRt, + errMsgCn: row.errMsgCn, }), { id: "anlsSttsNm", header: "진행 상태", cell: (info) => { - const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt } = info.getValue(); + const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt, errMsgCn } = + info.getValue(); if (anlsSttsCd === "PROCESSING") { return (
@@ -35,6 +44,23 @@ export const verificationHistoryColumns = [
); } + if (anlsSttsCd === "FAILED") { + return ( +
+ {anlsSttsNm} + + + + + +

+ {errMsgCn} +

+
+
+
+ ); + } return anlsSttsNm; }, }, From 7662e80ebaeb7655c82f71f269ac89b9539c5850 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 16:31:36 +0900 Subject: [PATCH 051/113] =?UTF-8?q?[st]=20feat:=20WithLoadingButton=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/detail/RegionalGrid.tsx | 10 +----- apps/st/src/shared/ui/with-loading-button.tsx | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 apps/st/src/shared/ui/with-loading-button.tsx diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index abfa0fdfb..db259df39 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -341,15 +341,7 @@ const RegionalGrid: React.FC = ({ onClick={handleDownload} disabled={!vrfcTrgtSn || isDownloading} > - {isDownloading ? ( -
-
- -
-
- ) : ( - "다운로드" - )} + 다운로드
diff --git a/apps/st/src/shared/ui/with-loading-button.tsx b/apps/st/src/shared/ui/with-loading-button.tsx new file mode 100644 index 000000000..384464954 --- /dev/null +++ b/apps/st/src/shared/ui/with-loading-button.tsx @@ -0,0 +1,32 @@ +import { + Button, + type ButtonProps, + LoadingWithAnimation, +} from "@common/components"; + +interface WithLoadingButtonProps extends ButtonProps { + isLoading: boolean; + children: React.ReactNode; +} + +function WithLoadingButton({ + isLoading, + children, + ...props +}: WithLoadingButtonProps) { + return ( + + ); +} + +export { WithLoadingButton }; From 5446a8a6568e5f46fe6f990c8beabed8797b09f4 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 12 Mar 2026 16:56:39 +0900 Subject: [PATCH 052/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VerificationArticleManagement.tsx | 42 ++++++- .../verification/VerificationTargetDialog.tsx | 114 ++++++++++++------ 2 files changed, 115 insertions(+), 41 deletions(-) diff --git a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx index b852b1496..d919a6823 100644 --- a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx +++ b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx @@ -108,6 +108,30 @@ function VerificationArticleManagement() { }); } + const saveMutation = useMutation({ + mutationFn: (data: VerificationTargetManagementResponse) => + verificationTargetManagementApi.saveVerificationTargetManagement({ + vrfcMthdCd: data.vrfcMthdCd || activeTab, + statsCd: data.statsCd, + schmNm: data.schmNm, + tblNm: data.tblNm, + colNm: data.colNm, + pkColNm: data.pkColNm, + ipcrCntVl: data.ipcrCntVl, + pctlCrtrVl: data.pctlCrtrVl, + scopCntCntVl: data.scopCntCntVl, + thrsCntVl: data.thrsCntVl, + useYn: data.useYn, + vrfcTrgtNm: data.vrfcTrgtNm, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG], + }); + setDialogOpen(false); + }, + }); + const updateMutation = useMutation({ mutationFn: (data: VerificationTargetManagementResponse) => verificationTargetManagementApi.updateVerificationTargetManagement( @@ -155,6 +179,11 @@ function VerificationArticleManagement() { setDialogOpen(true); } + function handleRegister() { + setSelectedData(null); + setDialogOpen(true); + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); handleSearch(); @@ -162,7 +191,11 @@ function VerificationArticleManagement() { async function handleSave(data: VerificationTargetManagementResponse) { if (await confirm("저장하시겠습니까?")) { - updateMutation.mutate(data); + if (data.vrfcTrgtSn) { + updateMutation.mutate(data); + } else { + saveMutation.mutate(data); + } } } @@ -219,6 +252,13 @@ function VerificationArticleManagement() {
+ - + {data && ( + + )} From 1d73da316c1d04ed67fd937eada96904c996c6bc Mon Sep 17 00:00:00 2001 From: jcchoi Date: Thu, 12 Mar 2026 22:54:01 -0400 Subject: [PATCH 053/113] =?UTF-8?q?ci:=20=EB=B0=B0=ED=8F=AC=20config=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 ++--- apps/common/features/next.config.ts | 15 +++++++ ecosystem.config.js | 68 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 ecosystem.config.js diff --git a/.gitignore b/.gitignore index fd688dd4c..bcf6dab44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ # dependencies node_modules/ .idea/ -# build -.next/ -dist/ # build .next @@ -22,6 +19,9 @@ apps/fo/.next/**/* #.env* .qodo +# next-agents-md +.next-docs/ + pnpm-lock.yaml settings.json .npmrc @@ -29,6 +29,4 @@ settings.json .agent apps/**/tsconfig.tsbuildinfo - -# next-agents-md -.next-docs/ +logs/ \ No newline at end of file diff --git a/apps/common/features/next.config.ts b/apps/common/features/next.config.ts index 34965cf32..8a50ea611 100644 --- a/apps/common/features/next.config.ts +++ b/apps/common/features/next.config.ts @@ -1,6 +1,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + output: "standalone", + experimental: { + proxyTimeout: 600000, + }, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, + logging: { + fetches: { + fullUrl: true, + }, + }, async rewrites() { return { afterFiles: [ diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 000000000..8c5873da0 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,68 @@ +const common = { + exec_mode: 'cluster', + instances: 'max', + + autorestart: true, + max_memory_restart: '1G', + watch: false, + + merge_logs: true, + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + + env: { + NODE_ENV: 'production', + }, +} + +module.exports = { + apps: [ + { + ...common, + + name: 'cs-app', + cwd: './apps/cs', + + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3000', + + error_file: './logs/cs-error.log', + out_file: './logs/cs-out.log', + }, + { + ...common, + + name: 'st-app', + cwd: './apps/st', + + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3001', + + error_file: './logs/st-error.log', + out_file: './logs/st-out.log', + }, + { + ...common, + + name: 'cm-app', + cwd: './apps/cm', + + script: 'node_modules/next/dist/bin/next', + args: 'start -p 3002', + + error_file: './logs/cm-error.log', + out_file: './logs/cm-out.log', + }, + { + ...common, + + name: 'features-app', + cwd: './apps/common/features', + + script: 'node_modules/next/dist/bin/next', + args: 'start -p 13000', + + error_file: './logs/feature-error.log', + out_file: './logs/feature-out.log', + }, + ], +} From 6b28c4e33213a045394deb7208e3706c082164f2 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 13 Mar 2026 14:20:36 +0900 Subject: [PATCH 054/113] =?UTF-8?q?[st]=20chore:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=83=81=ED=83=9C=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/detail/RegionalGrid.tsx | 15 ++++++++++++++- .../model/statistics-analysis-result-interface.ts | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index db259df39..5b0bffc8b 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -125,9 +125,22 @@ const RegionalGrid: React.FC = ({ } } + const mapOtlrToLabel = (otlrYn: string) => { + switch (otlrYn) { + case "0": + return "정상"; + case "1": + return "이상치"; + case "2": + return "제외"; + default: + return ""; + } + }; + const mappedData = data.map((item) => ({ ...item, - otlrYn: item.otlrYn === "0" ? "정상" : "이상치", + otlrYn: mapOtlrToLabel(item.otlrYn), })); setGridRowData( diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts index a2a3e5e84..8e91b7d02 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -20,7 +20,7 @@ export interface StAnlsRsltResponse { chgRt: number | null; // 변경율 (r_i) trsfRt: number | null; // 변환 비율 (s_i) otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i) - otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치) + otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외) } export interface StAnlsRsltSearchParams extends PageableParams { From f575e3b677b9b72341c3cc482a59c480eca20a81 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 13 Mar 2026 14:51:58 +0900 Subject: [PATCH 055/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=83=81=EC=84=B8=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=B0=A8=ED=8A=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/HbVerificationDetail.tsx | 22 ++++++++-- .../verification/detail/RegionalGrid.tsx | 27 ++++-------- .../verification/detail/ScatterChart.tsx | 41 ++++++++++++++----- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index 026ed7688..a9e267506 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -13,6 +13,12 @@ interface HbVerificationDetailProps { onBack: () => void; } +export interface AppliedFilter { + sido: string | null; + gugun: string; + status: string; +} + const HbVerificationDetail: React.FC = ({ anlsSn, onBack, @@ -20,7 +26,11 @@ const HbVerificationDetail: React.FC = ({ const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState( null, ); - const [selectedSido, setSelectedSido] = useState(null); + const [appliedFilter, setAppliedFilter] = useState({ + sido: "", + gugun: "", + status: "", + }); const [selectedRowId, setSelectedRowId] = useState(null); const [selectedTargetInfo, setSelectedTargetInfo] = useState<{ statsNm: string; @@ -34,6 +44,11 @@ const HbVerificationDetail: React.FC = ({ vrfcTrgtNm: row.vrfcTrgtNm, }); setSelectedRowId(null); + setAppliedFilter({ + sido: "", + gugun: "", + status: "", + }); }; return ( @@ -76,8 +91,9 @@ const HbVerificationDetail: React.FC = ({ vrfcTrgtNm={ selectedTargetInfo?.vrfcTrgtNm || "" } - onSidoSelect={setSelectedSido} + onFilterChange={setAppliedFilter} selectedRowId={selectedRowId} + appliedFilter={appliedFilter} />
@@ -89,7 +105,7 @@ const HbVerificationDetail: React.FC = ({
diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 5b0bffc8b..1a691283d 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -18,6 +18,8 @@ import { SelectValue, } from "@common/components"; +import type { AppliedFilter } from "./HbVerificationDetail"; + import DataGrid from "@/components/module/DataGrid"; import type { ColumnInfo } from "@/components/type/DataGrid.type"; @@ -26,14 +28,9 @@ interface RegionalGridProps { vrfcTrgtSn: string | null; statsNm: string; vrfcTrgtNm: string; - onSidoSelect: (sido: string | null) => void; + onFilterChange: (filter: AppliedFilter) => void; selectedRowId?: number | null; -} - -interface AppliedFilter { - sido: string | null; - gugun: string; - status: string; + appliedFilter: AppliedFilter; } const RegionalGrid: React.FC = ({ @@ -41,19 +38,15 @@ const RegionalGrid: React.FC = ({ vrfcTrgtSn, statsNm, vrfcTrgtNm, - onSidoSelect, + onFilterChange, selectedRowId, + appliedFilter, }) => { const [selectedSido, setSelectedSido] = useState(""); const [selectedGugun, setSelectedGugun] = useState(""); const [selectedStatus, setSelectedStatus] = useState(""); const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] = useState(true); - const [appliedFilter, setAppliedFilter] = useState({ - sido: "", - gugun: "", - status: "", - }); const [isDownloading, setIsDownloading] = useState(false); const [gridRowData, setGridRowData] = useState< @@ -244,8 +237,7 @@ const RegionalGrid: React.FC = ({ }; const handleSearch = () => { - onSidoSelect(selectedSido); - setAppliedFilter({ + onFilterChange({ sido: selectedSido, gugun: selectedGugun, status: selectedStatus, @@ -254,11 +246,10 @@ const RegionalGrid: React.FC = ({ }; const handleReset = () => { - onSidoSelect(null); setSelectedSido(""); setSelectedGugun(""); setSelectedStatus(""); - setAppliedFilter({ + onFilterChange({ sido: "", gugun: "", status: "", @@ -379,10 +370,10 @@ const RegionalGrid: React.FC = ({ columnInfo={columnInfo} rowData={gridRowData} setRowData={setGridRowData} + idFieldName="rsltSn" isExcelExport={false} isExcelImport={false} highlightedRowId={selectedRowId} - idFieldName="rsltSn" /> )}
diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 46aab674a..3f020ab93 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -26,6 +26,8 @@ import { import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { LoadingWithAnimation } from "@common/components"; +import type { AppliedFilter } from "./HbVerificationDetail"; + ChartJS.register( LinearScale, LineController, @@ -40,7 +42,7 @@ ChartJS.register( interface ScatterChartProps { anlsSn: number; vrfcTrgtSn: string | null; - selectedSido: string | null; + appliedFilter: AppliedFilter; onPointClick?: (rsltSn: number | null) => void; } @@ -56,7 +58,7 @@ interface DataPoint extends LinePoint { const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, - selectedSido, + appliedFilter, onPointClick, }) => { const { data: regionCodes } = useQuery({ @@ -64,12 +66,17 @@ const ScatterChart: React.FC = ({ queryFn: () => CodeApi.getCode("LAESECD"), }); - const selectedSidoName = useMemo(() => { - if (!selectedSido || !regionCodes) return null; - return ( - regionCodes.find((c) => c.lwpoCd === selectedSido)?.lwpoCdNm || null - ); - }, [regionCodes, selectedSido]); + const filterNames = useMemo(() => { + if (!regionCodes) return { sido: null, gugun: null }; + return { + sido: + regionCodes.find((c) => c.lwpoCd === appliedFilter.sido) + ?.lwpoCdNm || null, + gugun: + regionCodes.find((c) => c.lwpoCd === appliedFilter.gugun) + ?.lwpoCdNm || null, + }; + }, [regionCodes, appliedFilter.sido, appliedFilter.gugun]); const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList( anlsSn, @@ -85,7 +92,7 @@ const ScatterChart: React.FC = ({ const chartConfig = useMemo(() => { if (!resultData || !chartData) return null; - const rawPoints = resultData.content ?? []; + const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[]; const boundaries = chartData.list ?? []; const sortedBoundaries = [...boundaries].sort( @@ -106,7 +113,19 @@ const ScatterChart: React.FC = ({ const outlierPoints: DataPoint[] = []; rawPoints.forEach((item) => { - if (selectedSidoName && item.sido !== selectedSidoName) { + // Apply Filters (Sido, Gugun, Status) + if (filterNames.sido && item.sido !== filterNames.sido) { + return; + } + + if (filterNames.gugun && !filterNames.gugun.includes(item.gugun)) { + return; + } + + if ( + appliedFilter.status && + item.otlrYn.toLowerCase() !== appliedFilter.status + ) { return; } @@ -230,7 +249,7 @@ const ScatterChart: React.FC = ({ }; return { data, options }; - }, [resultData, chartData, selectedSidoName]); + }, [resultData, chartData, filterNames, appliedFilter.status]); return (
From 8269b31b42bf8af7fa728f43168fec615171810b Mon Sep 17 00:00:00 2001 From: jangwoong Date: Fri, 13 Mar 2026 16:58:19 +0900 Subject: [PATCH 056/113] =?UTF-8?q?[=ED=86=B5=EA=B3=84=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C]=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EB=93=B1=EB=A1=9D=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EC=BB=AC=EB=9F=BC=EB=AA=85=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/VerificationTargetDialog.tsx | 681 +++++++++++------- .../verification-target-management-service.ts | 13 + ...erification-target-management-interface.ts | 13 + 3 files changed, 441 insertions(+), 266 deletions(-) diff --git a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx index b59606c25..3c6043b27 100644 --- a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx +++ b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx @@ -1,12 +1,17 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Search } from "lucide-react"; import { CodeSelect } from "@/entities/code/ui/code-select"; -import type { VerificationTargetManagementResponse } from "@/entities/statistics/model/verification-target-management-interface"; +import type { + NumericColumnDto, + VerificationTargetManagementResponse, +} from "@/entities/statistics/model/verification-target-management-interface"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import { type VerificationTargetManagementFormData, verificationTargetManagementSchema, @@ -23,6 +28,7 @@ import { TableBody, TableCell, TableHead, + TableHeader, TableRow, } from "@common/components"; @@ -45,7 +51,7 @@ export function VerificationTargetDialog({ onSave, onDelete, }: VerificationTargetDialogProps) { - const { control, handleSubmit, reset } = + const { control, handleSubmit, reset, getValues, setValue } = useForm({ resolver: zodResolver(verificationTargetManagementSchema), defaultValues: data || { @@ -60,6 +66,11 @@ export function VerificationTargetDialog({ }, }); + const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false); + const [numericColumns, setNumericColumns] = useState( + [], + ); + useEffect(() => { if (open) { reset( @@ -90,321 +101,459 @@ export function VerificationTargetDialog({ } } + async function handleSearchTableMetadata() { + const statsCd = getValues("statsCd"); + if (!statsCd) { + alert("통계항목을 선택해주세요."); + return; + } + + try { + const metadata = + await verificationTargetManagementApi.getTableInfo(statsCd); + + setValue("schmNm", metadata.schmNm, { shouldValidate: true }); + setValue("tblNm", metadata.tblNm, { shouldValidate: true }); + + if (metadata.pkColumns && metadata.pkColumns.length > 0) { + setValue("pkColNm", metadata.pkColumns.join(", "), { + shouldValidate: true, + }); + } + + setNumericColumns(metadata.numericColumns || []); + setIsColumnDialogOpen(true); + } catch (error) { + alert("테이블 정보를 조회하는 중 오류가 발생했습니다."); + console.error(error); + } + } + + function handleSelectColumn(colNm: string) { + setValue("colNm", colNm, { shouldValidate: true }); + setIsColumnDialogOpen(false); + } + return ( - - - - - {data ? "검증 항목 상세" : "검증 항목 등록"} - - + <> + + + + + {data ? "검증 항목 상세" : "검증 항목 등록"} + + - - - - - - 통계항목 - - - ( -
- -
- )} - /> -
- - 검증명 - - - ( - +
+ + + + 통계항목 + + +
+ ( + + )} /> - )} - /> - - - - - - 스키마명 - - - ( -
- -
- )} - /> -
- - 테이블명 - - - ( -
- -
- )} - /> -
-
- - - - 컬럼명 - - - ( -
- -
- )} - /> -
- - PK 키 - - - ( -
- -
- )} - /> -
-
- - {tabValue !== "IMR" && ( - - - 중요도 계수(μ) + +
+
+ + 검증명 ( + + )} + /> + +
+ + + + 스키마명 + + + (
- field.onChange( - e.target.value - ? Number( - e - .target - .value, - ) - : null, - ) - } - placeholder="(0 ~ 1, 0.1 단위)" + {...field} + placeholder="스키마명" />
)} />
- - 백분위수 기준값(pct) + + 테이블명 (
- field.onChange( - e.target.value - ? Number( - e - .target - .value, - ) - : null, - ) - } - placeholder="(0 ~ 0.5, 0.1 단위)" + {...field} + placeholder="테이블명" />
)} />
- )} - {tabValue !== "IMR" && ( - - 스케일 조정 계수(A) + + 컬럼명 - ( -
+
+ ( - field.onChange( - e.target.value - ? Number( - e - .target - .value, - ) - : null, - ) - } - placeholder="(0 ~ 1, 0.01 단위)" + {...field} + placeholder="컬럼명" + className="flex-1" /> -
- )} - /> + )} + /> + +
- - 임계 계수(c) + + PK 키 (
- field.onChange( - e.target.value - ? Number( - e - .target - .value, - ) - : null, - ) - } - placeholder="(0 ~ 100, 1 단위)" + {...field} + placeholder="PK 키" />
)} />
- )} -
-
- - - {data && ( + {tabValue !== "IMR" && ( + + + 중요도 계수(μ) + + + ( +
+ + field.onChange( + e.target + .value + ? Number( + e + .target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 1, 0.1 단위)" + /> +
+ )} + /> +
+ + 백분위수 기준값(pct) + + + ( +
+ + field.onChange( + e.target + .value + ? Number( + e + .target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 0.5, 0.1 단위)" + /> +
+ )} + /> +
+
+ )} + + {tabValue !== "IMR" && ( + + + 스케일 조정 계수(A) + + + ( +
+ + field.onChange( + e.target + .value + ? Number( + e + .target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 1, 0.01 단위)" + /> +
+ )} + /> +
+ + 임계 계수(c) + + + ( +
+ + field.onChange( + e.target + .value + ? Number( + e + .target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 100, 1 단위)" + /> +
+ )} + /> +
+
+ )} + + + + - )} - - - -
-
+ {data && ( + + )} + + + +
+
+ + + + + 숫자형 컬럼 목록 + +
+ + + + + 컬럼명 + + + 데이터 타입 + + + 코멘트 + + + 선택 + + + + + {numericColumns.length === 0 ? ( + + + 조회된 숫자형 컬럼이 없습니다. + + + ) : ( + numericColumns.map((col, idx) => ( + + + {col.columnName} + + + {col.dataType} + + + {col.columnComment} + + + + + + )) + )} + +
+
+
+
+ ); } diff --git a/apps/st/src/entities/statistics/api/verification-target-management-service.ts b/apps/st/src/entities/statistics/api/verification-target-management-service.ts index f7430c28b..1cad4c110 100644 --- a/apps/st/src/entities/statistics/api/verification-target-management-service.ts +++ b/apps/st/src/entities/statistics/api/verification-target-management-service.ts @@ -12,6 +12,7 @@ import type { VerificationTargetManagementUpdateRequest, VerificationTargetManagementUpdateApiResponse, VerificationTargetManagementDeleteApiResponse, + TableMetadataResponse, } from "../model/verification-target-management-interface"; export class VerificationTargetManagementApiService implements VerificationTargetManagementService { @@ -44,6 +45,18 @@ export class VerificationTargetManagementApiService implements VerificationTarge return response.data; } + /** + * 테이블 메타데이터 조회 + * @param statsCd 통계항목 코드 + */ + public async getTableInfo(statsCd: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG}/table-info`, + { params: { statsCd } }, + ); + return response.data; + } + /** * 검증 대상 코드 목록 조회 */ diff --git a/apps/st/src/entities/statistics/model/verification-target-management-interface.ts b/apps/st/src/entities/statistics/model/verification-target-management-interface.ts index 5a7df41fd..b09454f99 100644 --- a/apps/st/src/entities/statistics/model/verification-target-management-interface.ts +++ b/apps/st/src/entities/statistics/model/verification-target-management-interface.ts @@ -1,4 +1,17 @@ import type { PageableList } from "@/shared/model"; + +export interface NumericColumnDto { + columnName: string; + dataType: string; + columnComment: string; +} + +export interface TableMetadataResponse { + schmNm: string; + tblNm: string; + pkColumns: string[]; + numericColumns: NumericColumnDto[]; +} import type { ApiResponse } from "./statistics-interface"; // 검증 대상 코드 응답 DTO From af12aa0718bd7715c1986ca50e9c01cc8b97b572 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Fri, 13 Mar 2026 17:23:07 +0900 Subject: [PATCH 057/113] =?UTF-8?q?[=ED=86=B5=EA=B3=84=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C]=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/VerificationTargetDialog.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx index 3c6043b27..c6898e8a0 100644 --- a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx +++ b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx @@ -85,6 +85,8 @@ export function VerificationTargetDialog({ useYn: "Y", }, ); + } else { + setNumericColumns([]); } }, [data, open, reset, tabValue]); @@ -129,6 +131,14 @@ export function VerificationTargetDialog({ } } + function handleOpenColumnDialog() { + if (numericColumns.length === 0) { + handleSearchTableMetadata(); + } else { + setIsColumnDialogOpen(true); + } + } + function handleSelectColumn(colNm: string) { setValue("colNm", colNm, { shouldValidate: true }); setIsColumnDialogOpen(false); @@ -273,9 +283,7 @@ export function VerificationTargetDialog({ type="button" variant="outline" className="whitespace-nowrap px-3" - onClick={() => - setIsColumnDialogOpen(true) - } + onClick={handleOpenColumnDialog} > 목록보기 From 3fdc459b16ba17efc99d73c644689ca1465c4a5c Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 16 Mar 2026 17:00:51 +0900 Subject: [PATCH 058/113] =?UTF-8?q?[=EC=82=AC=EC=9A=A9=EC=9E=90=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4]=20=ED=9A=8C=EC=9B=90=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EB=A1=9C=EA=B7=B8=20=EC=83=81=ED=83=9C=EA=B0=92?= =?UTF-8?q?=EC=97=90=20=EC=A0=84=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/loginlog/ui/loginlog-page.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index 7ae7990a1..d94d79c9a 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -39,10 +39,10 @@ function LoginlogPage() { const [presetDateRange, setPresetDateRange] = useState(""); const [dateRange, setDateRange] = useState(defaultDateRange); const [userId, setUserId] = useState(""); - const [errOcrnYn, setErrOcrnYn] = useState("N"); + const [errOcrnYn, setErrOcrnYn] = useState("ALL"); const [searchParams, setSearchParams] = useState({ userId: "", - errOcrnYn: "N", + errOcrnYn: "", searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"), searchDateTo: dayjs().format("YYYY-MM-DD"), sort: "lgnDt,desc", @@ -87,7 +87,7 @@ function LoginlogPage() { setSearchParams({ ...searchParams, userId, - errOcrnYn, + errOcrnYn: errOcrnYn === "ALL" ? "" : errOcrnYn, searchDateFrom: dateRange.from ? dayjs(dateRange.from).format("YYYY-MM-DD") : "", @@ -102,12 +102,12 @@ function LoginlogPage() { const handleReset = () => { setUserId(""); - setErrOcrnYn("N"); + setErrOcrnYn("ALL"); setDateRange(defaultDateRange); setSearchParams({ ...searchParams, userId: "", - errOcrnYn: "N", + errOcrnYn: "", searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"), searchDateTo: dayjs().format("YYYY-MM-DD"), }); @@ -180,6 +180,7 @@ function LoginlogPage() { + 전체 정상 오류 From 2cf241236e938b39ee6124989d5f79252f4af573 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 17 Mar 2026 09:29:01 +0900 Subject: [PATCH 059/113] =?UTF-8?q?[st]=20design:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EB=93=B1=EB=A1=9D=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20ui=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/VerificationTargetDialog.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx index c6898e8a0..ce9b1d26e 100644 --- a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx +++ b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx @@ -7,11 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Search } from "lucide-react"; import { CodeSelect } from "@/entities/code/ui/code-select"; +import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import type { NumericColumnDto, VerificationTargetManagementResponse, } from "@/entities/statistics/model/verification-target-management-interface"; -import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import { type VerificationTargetManagementFormData, verificationTargetManagementSchema, @@ -51,7 +51,7 @@ export function VerificationTargetDialog({ onSave, onDelete, }: VerificationTargetDialogProps) { - const { control, handleSubmit, reset, getValues, setValue } = + const { control, handleSubmit, reset, watch, getValues, setValue } = useForm({ resolver: zodResolver(verificationTargetManagementSchema), defaultValues: data || { @@ -65,6 +65,7 @@ export function VerificationTargetDialog({ useYn: "Y", }, }); + const watchStatsCd = watch("statsCd"); const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false); const [numericColumns, setNumericColumns] = useState( @@ -103,7 +104,8 @@ export function VerificationTargetDialog({ } } - async function handleSearchTableMetadata() { + async function handleSearchTableMetadata(e?: React.MouseEvent) { + e?.preventDefault(); const statsCd = getValues("statsCd"); if (!statsCd) { alert("통계항목을 선택해주세요."); @@ -131,7 +133,8 @@ export function VerificationTargetDialog({ } } - function handleOpenColumnDialog() { + function handleOpenColumnDialog(e?: React.MouseEvent) { + e?.preventDefault(); if (numericColumns.length === 0) { handleSearchTableMetadata(); } else { @@ -156,6 +159,12 @@ export function VerificationTargetDialog({
+ + + + + + @@ -280,10 +286,10 @@ export function VerificationTargetDialog({ )} /> @@ -542,16 +548,14 @@ export function VerificationTargetDialog({ From 80036ed876c5e0c475a435ea8e102f2626aadd3f Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 17 Mar 2026 10:03:35 +0900 Subject: [PATCH 060/113] =?UTF-8?q?feat:=20=ED=8E=8C=ED=94=84=EC=9E=A5=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/PumpingStationRvw.tsx | 2 +- .../column/review/PumpingStationRvwColumn.ts | 8 ++-- .../util/column/review/SewagePipeRvwColumn.ts | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx index 346a9ef33..fc5d8c2c6 100644 --- a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx +++ b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx @@ -85,7 +85,7 @@ const PumpingStationRvw = () => { return (
- +
diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index 3c22c517c..54dbfcedd 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -88,10 +88,10 @@ export const rawPumpingStationInfoList = { ] }, { name: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true }, - { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, // bscFctNm -> swtrmtFcltNm - { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, // pumpCls -> pumpSeNm - { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, // addr -> impdFcltPstnCn - { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, // rnDt -> oprtngStrtYmd + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, + { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, + { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, { name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index ae445b22d..1adb4eadf 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -1,8 +1,13 @@ import { ValueType } from "realgrid"; -const baseYear = new Date().getFullYear(); +import dayjs from 'dayjs'; import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; import type { GridView } from "@/components/type/DataGrid.type"; + +const currentYear = dayjs().year() - 1; +const startYear = 1996; +const totalCount = currentYear - startYear + 1 + 1; + // 하수도 통계 관리 02. 하수관로 const rawList = [ { @@ -248,7 +253,7 @@ const rawList = [ } ] }, - { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' },valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, @@ -273,19 +278,19 @@ const rawList = [ { name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, 'sggNm', 'admdstNm' ] }, 'swtrmtFcltNm','prcsFcltCpct', 'displayYear', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, // name 변경됨 + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, 'sprsysVartnRt', - { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, // name 변경됨 + { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, { name: 'pipeGroup', header: { text: '하수관로' }, items: [ 'wholFcltPrlg', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, // name 변경됨 + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, { name: 'siteGroup', header: { text: '토실/토구(개소)' }, items: [ @@ -383,18 +388,18 @@ const rawList = [ { name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 동적 연도 컬럼 (1996년이전[yy30] ~ 2025년[yy1]) */ - ...Array.from({ length: 30 }, (_, i) => { - const dtoNum = 30 - i; // 30, 29, ..., 1 + + ...Array.from({ length: totalCount }, (_, i) => { + const dtoNum = totalCount - i; - const wholField = `yy${dtoNum}PrlgWhol`; + const wholField = `yy${dtoNum}PrlgWhol`; const lyngField = `yy${dtoNum}LyngPrlg`; const dmltnField = `yy${dtoNum}DmltnPrlg`; - + return [ - { name: `${wholField}_col`, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `${lyngField}_col`, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `${dmltnField}_col`, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + { name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } ]; }).flat(), @@ -404,15 +409,17 @@ const rawList = [ 'displayYear', { name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum','lyngPrlgSum', 'dmltnPrlgSum'] }, - ...Array.from({ length: 30 }, (_, i) => { - const dtoNum = 30 - i; - const yearLabel = i === 0 ? '1996년이전' : `${1995 + i}년`; - const wholField = `yy${dtoNum}PrlgWhol`; + ...Array.from({ length: totalCount }, (_, i) => { + + const dtoNum = totalCount - i; + const yearLabel = i === 0 + ? '1996년이전' + : `${startYear + (i - 1)}년`; return { name: `group_${yearLabel}`, header: { text: yearLabel }, - items: [`${wholField}_col`, `yy${dtoNum}LyngPrlg_col`, `yy${dtoNum}DmltnPrlg_col`] + items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`] }; }), 'rmrk' @@ -512,7 +519,7 @@ const rawList = [ name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, 'sggNm', 'admdstNm', ] From 685ba15baa519b54871b7ae5939bb08fc2c8f16b Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 17 Mar 2026 10:07:08 +0900 Subject: [PATCH 061/113] =?UTF-8?q?st:=20feat:=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/SearchForm.tsx | 4 +- .../GeneralVerificationManagement.tsx | 271 +++++--- .../generalVerification/DetailComponent.tsx | 93 +++ .../generalVerification/RegistDialog.tsx | 613 ++++++++++++++++++ .../management/GeneralVerificationColumn.tsx | 45 +- apps/st/src/entities/code/index.ts | 1 + .../entities/code/ui/code-select-with-all.tsx | 48 ++ .../api/general-verification-service.ts | 66 ++ .../model/general-verification-interface.ts | 91 ++- ...neral-verification-master-submit-schema.ts | 28 + apps/st/src/shared/config/api.ts | 1 + 11 files changed, 1144 insertions(+), 117 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx create mode 100644 apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx create mode 100644 apps/st/src/entities/code/ui/code-select-with-all.tsx create mode 100644 apps/st/src/entities/statistics/api/general-verification-service.ts create mode 100644 apps/st/src/entities/statistics/model/general-verification-master-submit-schema.ts diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 54cdfe486..5bc809787 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -229,12 +229,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
{mergedProps.fctName &&
-
+ + + + + + + + + * 검증명 + + ( + + + + + {form.formState.errors.vrfcNm && ( +

+ {form.formState.errors.vrfcNm.message} +

+ )} +
+ )} + /> +
+ * 통계항목 + {(selectedVrfcRow as GeneralverificationList)?.trgtStatsNm ?? "-"} +
+
+
+
+ + +
+ + : + + } +
+
+ ); +}; + +export default DetailComponent; \ No newline at end of file diff --git a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx new file mode 100644 index 000000000..561bcaadf --- /dev/null +++ b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx @@ -0,0 +1,613 @@ +"use client"; + +import { useEffect, memo, useRef, useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + RadioGroup, + RadioGroupItem, + Label, + Select, + SelectTrigger, + SelectContent, + SelectValue, + SelectItem, + Skeleton, +} from "@common/components"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; +import { ENDPOINT } from "@/shared/config"; +import { CodeSelect } from "@/entities/code"; + +import { type GeneralVerificationMasterSubmitFormData, generalVerificationMasterSubmitSchema } from '@/entities/statistics/model/general-verification-master-submit-schema'; +import type { UpdateVrfcMasterRequest, RegistVrfcMasterRequest } from "@/entities/statistics/model/general-verification-interface"; +import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service'; +import { UserApi } from "@/entities/user/api/user-service"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useAlertStore } from "@/shared/lib"; +import { getUserInfo } from "../../../../../../../packages/common/components/src/api"; + +interface RegistDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + children?: React.ReactNode; + vrfcSn?: string; + /** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */ + onSuccess?: () => void; +} + +const RegistDialog = memo( + ({ + children, + open, + onOpenChange, + vrfcSn, + onSuccess, + ...props + }: RegistDialogProps) => { + + const isVrfcSn = vrfcSn ? true : false; + const [checkVrfcNm, setCheckVrfcNm] = useState(false); + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: detailData } = useQuery({ + queryKey: [`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, vrfcSn], + queryFn: () => generalverificationApiService.getGeneralverificationInfo(vrfcSn!), + enabled: open && !!vrfcSn, + }); + + const form = useForm({ + resolver: zodResolver(generalVerificationMasterSubmitSchema), + values: { + vrfcSn: detailData?.data.vrfcSn ?? '', + vrfcNm: detailData?.data.vrfcNm ?? '', + vrfcClsfCd: detailData?.data.vrfcClsfCd ?? '', + trgtStatsCd: detailData?.data.trgtStatsCd ?? '', + trgtStatsNm: detailData?.data.trgtStatsNm ?? '', + dcptDigtCnt: detailData?.data.dcptDigtCnt ?? 0, + dcptPrcsMthCd: detailData?.data.dcptPrcsMthCd ?? '', + useYn: detailData?.data.useYn ?? 'Y', + rgtrId: detailData?.data.rgtrId ?? '', + regDt: detailData?.data.regDt ?? '', + mdfrId: detailData?.data.mdfrId ?? '', + mdfcnDt: detailData?.data.mdfcnDt ?? '', + }, + }); + + const dcptDigtCnt = form.watch('dcptDigtCnt'); + const rgtrId = form.watch('rgtrId'); + const mdfrId = form.watch('mdfrId'); + const trgtStatsCd = form.watch('trgtStatsCd'); + const vrfcNm = form.watch('vrfcNm'); + const vrfcClsfCd = form.watch('vrfcClsfCd'); + + const { data: registrarUser } = useQuery({ + queryKey: ['user', 'rgtr', rgtrId], + queryFn: () => UserApi.findByUserUnqId(rgtrId!), + enabled: open && !!rgtrId, + }); + + const { data: modifierUser } = useQuery({ + queryKey: ['user', 'mdfr', mdfrId], + queryFn: () => UserApi.findByUserUnqId(mdfrId!), + enabled: open && !!mdfrId, + }); + + const originalVrfcNmRef = useRef(null); + + useEffect(() => { + if (detailData?.data.vrfcNm && originalVrfcNmRef.current == null) { + originalVrfcNmRef.current = detailData.data.vrfcNm; + } + }, [detailData]); + + /** 중복 여부 반환: true = 중복, false = 사용 가능 */ + const runVerificationNameCheck = async (trgtStatsCdVal: string, vrfcNmVal: string, vrfcClsfCd: string): Promise => { + try { + const response = await generalverificationApiService.checkVerificationName({ + trgtStatsCd: trgtStatsCdVal, + vrfcNm: vrfcNmVal, + vrfcClsfCd: vrfcClsfCd, + }); + if (response.status === 'success') { + const isDuplicate = response.data; + setCheckVrfcNm(isDuplicate); + return isDuplicate; + } + alert('중복검사 중 오류가 발생했습니다.'); + setCheckVrfcNm(true); + return true; + } catch { + alert('중복검사 중 오류가 발생했습니다.'); + setCheckVrfcNm(true); + return true; + } + }; + + const handleUpdateVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => { + try { + const response = await generalverificationApiService.UpdateVrfcMaster(data as UpdateVrfcMasterRequest); + + if (response.status === 'success') { + alert('수정되었습니다.'); + onOpenChange(false); + onSuccess?.(); + } else { + alert(response.message ?? '수정 중 오류가 발생했습니다.'); + } + } catch { + alert('수정 중 오류가 발생했습니다.'); + } + }; + + const handleRegistVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => { + try { + const response = await generalverificationApiService.RegistVrfcMaster(data as RegistVrfcMasterRequest); + + if (response.status === 'success') { + alert('등록되었습니다.'); + onOpenChange(false); + onSuccess?.(); + } else { + alert(response.message ?? '등록 중 오류가 발생했습니다.'); + } + } catch { + alert('등록 중 오류가 발생했습니다.'); + } + }; + + const handleDeleteVrfcMaster = async () => { + const deleteMsg = `${detailData?.data.vrfcNm} 검증룰을 + 삭제하시겠습니까?`; + + if (await confirm(deleteMsg)) { + try { + const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn || ''); + + if (response.status === 'success') { + alert('삭제되었습니다.'); + onOpenChange(false); + onSuccess?.(); + } else { + alert(response.message ?? '삭제 중 오류가 발생했습니다.'); + } + } catch { + alert('삭제 중 오류가 발생했습니다.'); + } + } + }; + + useEffect(() => { + if (!open) return; + if (!vrfcNm) return; + + const original = originalVrfcNmRef.current; + // 수정 모드: 기준값이 아직 안 세팅됐으면 검사 안 함(다이얼로그 막 열었을 때) + if (isVrfcSn && original === null) return; + // 수정 모드: 사용자가 검증명을 바꾸지 않았으면 검사 안 함 + if (original !== null && original === vrfcNm) { + setCheckVrfcNm(false); + return; + } + + // const timer = setTimeout(() => { + // runVerificationNameCheck(trgtStatsCd ?? '', vrfcNm, vrfcClsfCd); + // }, 500); + + // return () => clearTimeout(timer); + }, [vrfcNm, open, isVrfcSn]); + + const onSubmit = async (data: GeneralVerificationMasterSubmitFormData) => { + const currentVrfcNm = (data.vrfcNm ?? '').trim(); + const originalVrfcNm = originalVrfcNmRef.current; + + // 수정 모드인데 기존값과 동일하면 중복검사 완전 생략 + const skipNameCheck = + isVrfcSn && + (originalVrfcNm === null || currentVrfcNm === originalVrfcNm); + + if (!skipNameCheck && currentVrfcNm) { + // 제출 시점에 중복검사 실행 + const isDuplicate = await runVerificationNameCheck( + data.trgtStatsCd ?? '', + currentVrfcNm, + data.vrfcClsfCd, + ); + if (isDuplicate) { + return; + } + } + + const confirmMessage = isVrfcSn + ? "수정 하시겠습니까?" + : "등록 하시겠습니까?"; + if (await confirm(confirmMessage)) { + const userInfo = await getUserInfo(); + if (isVrfcSn) { + const updateData = { + vrfcSn: data.vrfcSn, + vrfcNm: data.vrfcNm, + vrfcClsfCd: data.vrfcClsfCd, + dcptDigtCnt: data.dcptDigtCnt, + ...(data.dcptDigtCnt === 0 + ? {} + : { dcptPrcsMthCd: data.dcptPrcsMthCd }), + trgtStatsCd: data.trgtStatsCd, + mdfrId: userInfo.userUnqId, + }; + await handleUpdateVrfcMaster(updateData); + } else { + const registData = { + vrfcNm: data.vrfcNm, + vrfcClsfCd: data.vrfcClsfCd, + dcptDigtCnt: data.dcptDigtCnt, + dcptPrcsMthCd: data.dcptPrcsMthCd, + trgtStatsCd: data.trgtStatsCd, + rgtrId: userInfo.userUnqId + }; + await handleRegistVrfcMaster(registData); + } + } + }; + + useEffect(() => { + if (!open) { + form.reset(); + setCheckVrfcNm(false); + originalVrfcNmRef.current = null; + } + }, [open, form]); + + return ( + + {children} + + + + {isVrfcSn ? + detailData ? detailData.data.vrfcNm + : + + : "일반 검증 등록"} + + +
+ { + void onSubmit(data); + })} + > + + + + + + + + * 통계항목 + + ( + + + + + {form.formState.errors.trgtStatsCd && ( +

+ {form.formState.errors.trgtStatsCd.message} +

+ )} +
+ )} + /> +
+
+ + * 검증명 + + ( + + + + + {checkVrfcNm && +

+ 이미 등록된 검증명입니다. +

+ } + {form.formState.errors.vrfcNm && ( +

+ {form.formState.errors.vrfcNm.message} +

+ )} +
+ )} + /> + {/* */} +
+
+ + * 검증분류 + + ( + + + + + {form.formState.errors.vrfcClsfCd && ( +

+ {form.formState.errors.vrfcClsfCd.message} +

+ )} +
+ )} + /> +
+
+ + * 소수점자리수 + + ( + + + + +

+ ※ 0일시 소수점자리 생략 +

+ {form.formState.errors.dcptDigtCnt && ( +

+ {form.formState.errors.dcptDigtCnt.message} +

+ )} +
+ )} + /> +
+
+ + 소수점처리방식 + + ( + + + + +

+ ※ 소수점자리가 1일시 두 번째 자리에서 올림/반올림/내림 처리
+ 예시 - 14.16 → 14.2 +

+ {form.formState.errors.dcptPrcsMthCd && ( +

+ {form.formState.errors.dcptPrcsMthCd.message} +

+ )} +
+ )} + /> +
+
+ {isVrfcSn && + <> + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+ {form.formState.errors.useYn && ( +

+ {form.formState.errors.useYn.message} +

+ )} +
+ )} + /> +
+
+ + 등록자 + + ( + + + {registrarUser?.userName != null ? ( + {registrarUser.userName} + ) : field.value ? ( + + ) : ( + - + )} + + + )} + /> + + + + 등록일 + + ( + + + {field.value != null && field.value != '' ? + {field.value} + : + - + } + + + )} + /> + + + + 수정자 + + ( + + + {modifierUser?.userName != null ? ( + {modifierUser.userName} + ) : field.value ? ( + + ) : ( + - + )} + + + )} + /> + + + + 수정일 + + ( + + + {field.value != null && field.value != '' ? + {field.value} + : + - + } + + + )} + /> + + + + } +
+
+ + {vrfcSn && + + } + + + + + +
+ +
+
+ ); + }) + +export default RegistDialog; \ No newline at end of file diff --git a/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx b/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx index b61ad9ea3..4ed1b8825 100644 --- a/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx +++ b/apps/st/src/components/util/column/management/GeneralVerificationColumn.tsx @@ -1,10 +1,14 @@ import dayjs from "dayjs"; import { useState } from "react"; import type { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@common/components"; import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface"; -export const GeneralVerificationColumn: ColumnDef[] = [ +import { Button } from "@common/components"; +import RegistDialog from "@/components/statistics/verification/generalVerification/RegistDialog"; + +export const getGeneralVerificationColumns = ( + onUpdateSuccess?: () => void, +): ColumnDef[] => [ { id: "no", header: "NO", @@ -14,21 +18,20 @@ export const GeneralVerificationColumn: ColumnDef[] = [ return totalRows - (pageIndex * pageSize + row.index); }, }, + // { + // accessorKey: "vrfcSn", + // header: "일련번호", + // }, { - accessorKey: "vrfSn", - header: "일련번호", - enableHiding: true, - }, - { - accessorKey: "tabNm", + accessorKey: "trgtStatsNm", header: "통계항목", }, { - accessorKey: "vrfNm", + accessorKey: "vrfcNm", header: "검증명", }, { - accessorKey: "vrfClsCd", + accessorKey: "vrfcClsfNm", header: "검증분류", }, { @@ -48,15 +51,31 @@ export const GeneralVerificationColumn: ColumnDef[] = [ header: "관리", cell: ({ row }) => (
e.stopPropagation()}> - +
), }, ]; -const GeneralVerificationManageButton = ({ vrfSn }: { vrfSn: number }) => { +const GeneralVerificationManageButton = ({ + vrfcSn, + onSuccess, +}: { + vrfcSn: string; + onSuccess?: () => void; +}) => { const [dialogOpen, setDialogOpen] = useState(false); return ( - + + + ); }; diff --git a/apps/st/src/entities/code/index.ts b/apps/st/src/entities/code/index.ts index e166e2c29..cd4a3ea80 100644 --- a/apps/st/src/entities/code/index.ts +++ b/apps/st/src/entities/code/index.ts @@ -1,6 +1,7 @@ export { CodeApi } from "./api/code-service"; export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; +export { CodeSelectWithAll } from "./ui/code-select-with-all"; export { UpCodeApi } from "./api/upcode-service"; export { diff --git a/apps/st/src/entities/code/ui/code-select-with-all.tsx b/apps/st/src/entities/code/ui/code-select-with-all.tsx new file mode 100644 index 000000000..b6e62e4f0 --- /dev/null +++ b/apps/st/src/entities/code/ui/code-select-with-all.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { + cn, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "../api/code-service"; + +type CodeSelectProps = { + upCd: string; + placeholder?: string; + className?: string; +} & React.ComponentPropsWithoutRef; + +function CodeSelectWithAll({ + upCd, + placeholder = "선택", + className, + ...props +}: CodeSelectProps) { + const { data: code } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd], + queryFn: () => CodeApi.getCode(upCd), + enabled: !!upCd, + }); + + return ( + + ); +} + +export { CodeSelectWithAll }; diff --git a/apps/st/src/entities/statistics/api/general-verification-service.ts b/apps/st/src/entities/statistics/api/general-verification-service.ts new file mode 100644 index 000000000..e8128ebbe --- /dev/null +++ b/apps/st/src/entities/statistics/api/general-verification-service.ts @@ -0,0 +1,66 @@ +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + GeneralverificationRequest, + GeneralverificationResponse, + GeneralverificationService, + GeneralverificationInfoResponse, + CheckVerificationNameRequest, + CheckVerificationNameResponse, + UpdateVrfcMasterRequest, + UpdateVrfcMasterResponse, + RegistVrfcMasterRequest, + RegistVrfcMasterResponse, + DeleteVrfcMasterResponse, +} from "../model/general-verification-interface"; + +export class GeneralverificationApiService implements GeneralverificationService { + public async getGeneralverificationList(param: GeneralverificationRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, + { params: param }, + ); + return response.data; + } + + public async getGeneralverificationInfo(param: string): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`, + ); + return response.data; + } + + public async checkVerificationName(param: CheckVerificationNameRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`, + { params: param }, + ); + return response.data; + } + + public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise { + const response = await stClient.put ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`, + param, + ); + return response.data; + } + + public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise { + const response = await stClient.post ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`, + param, + ); + return response.data; + } + + public async DeleteVrfcMaster(param: string): Promise { + const response = await stClient.delete ( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`, + ); + return response.data; + } +} + +export const generalverificationApiService = new GeneralverificationApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/general-verification-interface.ts b/apps/st/src/entities/statistics/model/general-verification-interface.ts index c22504827..cebd65e7b 100644 --- a/apps/st/src/entities/statistics/model/general-verification-interface.ts +++ b/apps/st/src/entities/statistics/model/general-verification-interface.ts @@ -1,12 +1,89 @@ export interface GeneralverificationList { - vrfSn: number; - tabNm: string; - vrfNm: string; - vrfClsCd: string; - statsCd: string; + vrfcSn: string; + vrfcNm: string; + vrfcClsfCd: string; + vrfcClsfNm: string; + trgtStatsCd: string; + trgtStatsNm: string; + dcptDigtCnt: number; + dcptPrcsMthCd: string; + dcptPrcsMthNm: string; useYn: string; - rgtrId?: string; regDt?: string; - mdfrId?: string; mdfcnDt?: string; + rgtrId?: string; + mdfrId?: string; +} + +export interface GeneralverificationRequest { + trgtStatsCd?: string; + vrfcClsfCd?: string; + useYn?: string; +} + +export interface GeneralverificationResponse { + status: string; + message?: string; + data: GeneralverificationList[]; +} + +export interface GeneralverificationInfoResponse { + status: string; + message?: string; + data: GeneralverificationList; +} + +export interface CheckVerificationNameRequest { + trgtStatsCd: string; + vrfcNm: string; + vrfcClsfCd: string; +} + +export interface CheckVerificationNameResponse { + status: string; + message?: string; + data: boolean; +} + +export interface UpdateVrfcMasterRequest { + vrfcSn?: string | null; + vrfcNm?: string | null; + vrfcClsfCd?: string | null; + dcptDigtCnt?: number | null; + dcptPrcsMthCd?: string | null; + useYn?: string | null; + mdfrId?: string | null; +} + +export interface UpdateVrfcMasterResponse { + status: string; + message?: string; +} + +export interface RegistVrfcMasterRequest { + vrfcNm?: string | null; + vrfcClsfCd?: string | null; + dcptDigtCnt?: number | null; + dcptPrcsMthCd?: string | null; + rgtrId?: string | null; + trgtStatsCd?: string | null; +} + +export interface RegistVrfcMasterResponse { + status: string; + message?: string; +} + +export interface DeleteVrfcMasterResponse { + status: string; + message?: string; +} + +export interface GeneralverificationService { + getGeneralverificationList(param: GeneralverificationRequest): Promise; + getGeneralverificationInfo(param: string): Promise; + checkVerificationName(param: CheckVerificationNameRequest): Promise; + UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise; + RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise; + DeleteVrfcMaster(param: string): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/general-verification-master-submit-schema.ts b/apps/st/src/entities/statistics/model/general-verification-master-submit-schema.ts new file mode 100644 index 000000000..9118504da --- /dev/null +++ b/apps/st/src/entities/statistics/model/general-verification-master-submit-schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const generalVerificationMasterSubmitSchema = z + .object({ + vrfcSn: z.string().nullable().optional(), + vrfcNm: z.string().min(1, "검증명을 입력하세요"), + vrfcClsfCd: z.string().min(1, "검증분류를 선택하세요"), + trgtStatsCd: z.string().min(1, "통계항목을 선택하세요"), + trgtStatsNm: z.string().nullable().optional(), + dcptDigtCnt: z.coerce.number(), + dcptPrcsMthCd: z.string().nullable().optional(), + useYn: z.string().nullable().optional(), + rgtrId: z.string().nullable().optional(), + regDt: z.string().nullable().optional(), + mdfrId: z.string().nullable().optional(), + mdfcnDt: z.string().nullable().optional(), + }) + .refine( + (data) => data.dcptDigtCnt === 0 || !!data.dcptPrcsMthCd, + { + path: ["dcptPrcsMthCd"], + message: "소수점 처리방식을 선택하세요", + }, + ); + +export type GeneralVerificationMasterSubmitFormData = z.infer< + typeof generalVerificationMasterSubmitSchema +>; \ No newline at end of file diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 8fa5dce05..69cdaaf92 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -71,6 +71,7 @@ export const ENDPOINT = { INPUT: "/st-input-service/api/v1/input", REVIEW: "/st-management-service/api/v1/review", INPUTCOMMON: "/st-input-service/api/v1/common", + INPUTVERIFY: "/st-input-service/api/v1/verify", }, STATVERIFY_SERVICE: { VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng", From 414a69c6ad309f95aa13f02fd7de943f2543d537 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 17 Mar 2026 10:42:03 +0900 Subject: [PATCH 062/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=8C=80=EC=83=81=20=ED=86=B5=EA=B3=84=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=84=A0=ED=83=9D/=ED=95=B4=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 93 ++++++++++++------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 99a4a6e0b..07c2663c9 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -165,8 +165,16 @@ const HbStatisticsVerification = () => { [filteredVerificationTargetCodes], ); + const isAllSelected = useMemo(() => { + const count = filteredVerificationTargetCodes?.length ?? 0; + return count > 0 && selectedTargets.length === count; + }, [filteredVerificationTargetCodes, selectedTargets]); + + const handleToggleAll = () => { + setSelectedTargets(isAllSelected ? [] : allVerificationTargetIds); + }; + useEffect(() => { - if (!allVerificationTargetIds.length) return; setSelectedTargets(allVerificationTargetIds); }, [allVerificationTargetIds]); @@ -476,42 +484,59 @@ const HbStatisticsVerification = () => { filteredVerificationTargetCodes && filteredVerificationTargetCodes.length > 0 ? ( -
- {filteredVerificationTargetCodes.map( - (item) => ( -
- +
+ + +
+
+ {filteredVerificationTargetCodes.map( + (item) => ( +
- handleTargetToggle( - item.vrfcTrgtSn, - ) - } - /> - -
- ), - )} -
+ + handleTargetToggle( + item.vrfcTrgtSn, + ) + } + /> + +
+ ), + )} +
+ ) : (

From 1bc03d99c6da85c08e6bac638033d5b4278ac303 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 17 Mar 2026 10:47:51 +0900 Subject: [PATCH 063/113] =?UTF-8?q?st:=20fix:=20eslint=20error=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 2 +- .../statistics/input/SewagePipeInp.tsx | 64 +++++++++---------- .../statistics/management/LogHistory.tsx | 1 - .../generalVerification/DetailComponent.tsx | 4 +- .../generalVerification/RegistDialog.tsx | 5 +- .../model/verification-article-mock.ts | 4 +- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 36a17b225..4378182cf 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -67,7 +67,7 @@ const CompactDataGrid = ({ fixedColumns, showHeaderCheckbox = false, rowData, - setRowData, + // setRowData, getGridDataRef, cellStyleCallback, onRowClick, diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 20bed7f23..bdd404b0e 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -17,9 +17,9 @@ import { useAdminStore } from "@/store/admin-store"; const SewagePipeInp = () => { const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const { setMessage: alert } = useAlertStore((state) => state); - const [isLoading, setIsLoading] = useState(false); + const [_isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"); - const [isEnterable, setIsEnterable] = useState(true); + const [isEnterable, _setIsEnterable] = useState(true); const { isAdminInfo } = useAdminStore(); const columnInfo = useMemo(() => { @@ -46,42 +46,42 @@ const SewagePipeInp = () => { }); }, [isEnterable, isAdminInfo]); - const handleSearch = async (values: SearchFormValues) => { - if (!values.sido || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); - return; - } + // const handleSearch = async (values: SearchFormValues) => { + // if (!values.sido || values.sido === 'ALL') { + // alert('시도를 선택해주세요.'); + // return; + // } - setActiveTab('ST00020001'); + // setActiveTab('ST00020001'); - setRowDataMap(new Map()); + // setRowDataMap(new Map()); - try { - setIsLoading(true); + // try { + // setIsLoading(true); - // const searchParam: SewagePipeReviewRequestParams = { - // statsYr: values.year, - // sewerBzmnCd: values.bsnmCd, - // ctpvNm: values.sido, - // sggNm: values.gugun === 'ALL' ? '' : values.gugun - // }; + // // const searchParam: SewagePipeReviewRequestParams = { + // // statsYr: values.year, + // // sewerBzmnCd: values.bsnmCd, + // // ctpvNm: values.sido, + // // sggNm: values.gugun === 'ALL' ? '' : values.gugun + // // }; - // searchParamRef.current = searchParam; - // const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); + // // searchParamRef.current = searchParam; + // // const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam); - // if (response?.status === 'success' && response.data) { - // const newRowDataMap = new Map[]>(); - // newRowDataMap.set('ST00020001', toGridRows(response.data as object[])); - // setRowDataMap(newRowDataMap); - // } else { - // alert(response?.message || "데이터가 없습니다."); - // } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - }; + // // if (response?.status === 'success' && response.data) { + // // const newRowDataMap = new Map[]>(); + // // newRowDataMap.set('ST00020001', toGridRows(response.data as object[])); + // // setRowDataMap(newRowDataMap); + // // } else { + // // alert(response?.message || "데이터가 없습니다."); + // // } + // } catch { + // alert('데이터 조회 중 오류가 발생했습니다.'); + // } finally { + // setIsLoading(false); + // } + // }; const SubmitExceptionFunc = () => { console.log('submit exception'); diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index 0b45ce9fd..2938343f8 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -18,7 +18,6 @@ import DataGrid from '@/components/module/DataGrid'; import { CodeSelect } from '@/entities/code'; import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn'; -import { useAlertStore } from "@/shared/lib"; // import { LwpoCodeApi } from "@/entities/code"; // const getStateCdListFetcher = async () => { diff --git a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx index 34409059b..752c6ff6b 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx @@ -16,8 +16,8 @@ import { } from "@common/components"; import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; -import { useForm, Controller, type UseFormReturn } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { type UseFormReturn } from "react-hook-form"; + import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface'; import { type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema'; diff --git a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx index 561bcaadf..3a5bbc01e 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, memo, useRef, useState } from "react"; -import { useForm, Controller } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -95,7 +95,6 @@ const RegistDialog = memo( const mdfrId = form.watch('mdfrId'); const trgtStatsCd = form.watch('trgtStatsCd'); const vrfcNm = form.watch('vrfcNm'); - const vrfcClsfCd = form.watch('vrfcClsfCd'); const { data: registrarUser } = useQuery({ queryKey: ['user', 'rgtr', rgtrId], @@ -610,4 +609,6 @@ const RegistDialog = memo( ); }) +RegistDialog.displayName = 'RegistDialog'; + export default RegistDialog; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/verification-article-mock.ts b/apps/st/src/entities/statistics/model/verification-article-mock.ts index 272efeb16..f4986656c 100644 --- a/apps/st/src/entities/statistics/model/verification-article-mock.ts +++ b/apps/st/src/entities/statistics/model/verification-article-mock.ts @@ -93,7 +93,7 @@ function formatDateTime(date: Date): string { /** H-B 탭 목데이터 생성 */ export function createHBArticleMock(count = 10): HBArticleItem[] { faker.seed(100); - return Array.from({ length: count }, (_, index) => ({ + return Array.from({ length: count }, (_, _index) => ({ statsItmNm: faker.helpers.arrayElement(HB_STATS_ITEMS), vrfcItmNm: faker.helpers.arrayElement(HB_VERIFICATION_ITEMS), tblNm: faker.helpers.arrayElement(TABLE_NAMES), @@ -113,7 +113,7 @@ export function createHBArticleMock(count = 10): HBArticleItem[] { /** I-MR 탭 목데이터 생성 */ export function createIMRArticleMock(count = 10): IMRArticleItem[] { faker.seed(200); - return Array.from({ length: count }, (_, index) => ({ + return Array.from({ length: count }, (_, _index) => ({ statsItmNm: faker.helpers.arrayElement(IMR_STATS_ITEMS), vrfcNm: faker.helpers.arrayElement(VERIFICATION_NAMES), tblNm: faker.helpers.arrayElement(TABLE_NAMES), From ee2301470a0149336a39566a84c285b1878a316c Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 17 Mar 2026 14:51:30 +0900 Subject: [PATCH 064/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/HbVerificationDetail.tsx | 3 + .../verification/detail/RegionalGrid.tsx | 86 +------------- .../verification/detail/VerificationList.tsx | 86 +++----------- .../statistics/model/regional-grid-columns.ts | 83 ++++++++++++++ .../model/verification-list-columns.tsx | 108 ++++++++++++++++++ 5 files changed, 210 insertions(+), 156 deletions(-) create mode 100644 apps/st/src/entities/statistics/model/regional-grid-columns.ts create mode 100644 apps/st/src/entities/statistics/model/verification-list-columns.tsx diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index a9e267506..c95b771aa 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -26,6 +26,7 @@ const HbVerificationDetail: React.FC = ({ const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState( null, ); + const [checkedSns, setCheckedSns] = useState([]); const [appliedFilter, setAppliedFilter] = useState({ sido: "", gugun: "", @@ -75,6 +76,8 @@ const HbVerificationDetail: React.FC = ({ anlsSn={anlsSn} selectedVrfcTrgtSn={selectedVrfcTrgtSn} onSelect={handleTargetSelect} + checkedSns={checkedSns} + onCheck={setCheckedSns} />

diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 1a691283d..1850ac456 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { ValueType } from "realgrid"; import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; import { useStAnlsRsltList } from "@/entities/statistics/api/queries"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { Button, @@ -21,7 +21,6 @@ import { import type { AppliedFilter } from "./HbVerificationDetail"; import DataGrid from "@/components/module/DataGrid"; -import type { ColumnInfo } from "@/components/type/DataGrid.type"; interface RegionalGridProps { anlsSn: number; @@ -148,89 +147,6 @@ const RegionalGrid: React.FC = ({ gugunOptions, ]); - const columnInfo: ColumnInfo = useMemo( - () => ({ - columns: [ - { - name: "sido", - fieldName: "sido", - header: { text: "시도" }, - width: 80, - }, - { - name: "gugun", - fieldName: "gugun", - header: { text: "구군" }, - width: 80, - }, - { - name: "admdstNm", - fieldName: "admdstNm", - header: { text: "행정구역" }, - width: 100, - }, - { - name: "prvyrVl", - fieldName: "prvyrVl", - header: { text: "이전년도" }, - width: 80, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "cryrVl", - fieldName: "cryrVl", - header: { text: "당해년도" }, - width: 80, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "chgRt", - fieldName: "chgRt", - header: { text: "R" }, - width: 60, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { - name: "trsfRt", - fieldName: "trsfRt", - header: { text: "S" }, - width: 60, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { - name: "otlrTrsfRt", - fieldName: "otlrTrsfRt", - header: { text: "E" }, - width: 60, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { - name: "otlrYn", - fieldName: "otlrYn", - header: { text: "상태" }, - width: 60, - }, - ], - columnLayout: [ - "sido", - "gugun", - "admdstNm", - "prvyrVl", - "cryrVl", - "chgRt", - "trsfRt", - "otlrTrsfRt", - "otlrYn", - ], - }), - [], - ); - const handleSidoChange = (val: string) => { setSelectedSido(val); setSelectedGugun(""); diff --git a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx index 5c2f89065..84f3b17ba 100644 --- a/apps/st/src/components/statistics/verification/detail/VerificationList.tsx +++ b/apps/st/src/components/statistics/verification/detail/VerificationList.tsx @@ -2,44 +2,31 @@ import React, { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { - type ColumnDef, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { ENDPOINT } from "@/shared/config"; import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api"; -import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface"; +import { getVerificationListColumns } from "@/entities/statistics/model/verification-list-columns"; import { DataTable } from "@common/components"; interface VerificationListProps { anlsSn: number; selectedVrfcTrgtSn: string | null; onSelect: (row: StAnlsDtlResponse) => void; + checkedSns: string[]; + onCheck: (checkedSns: string[], rows: StAnlsDtlResponse[]) => void; } const VerificationList: React.FC = ({ anlsSn, selectedVrfcTrgtSn, onSelect, + checkedSns, + onCheck, }) => { - const { data: verificationTargetCodes } = useQuery({ - queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"], - queryFn: () => - verificationTargetManagementApi.getVerificationTargetCodes(), - }); - - const targetCodeMap = useMemo(() => { - const map = new Map(); - if (verificationTargetCodes) { - verificationTargetCodes.forEach((item) => { - map.set(item.vrfcTrgtSn, item.vrfcTrgtNm); - }); - } - return map; - }, [verificationTargetCodes]); - const { data: detailData, isLoading } = useQuery({ queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn], queryFn: () => @@ -51,50 +38,9 @@ const VerificationList: React.FC = ({ return (detailData?.content ?? []) as StAnlsDtlResponse[]; }, [detailData]); - const columns: ColumnDef[] = useMemo( - () => [ - { - header: "통계 년도", - accessorKey: "statsYr", - }, - { - header: "통계 항목", - accessorKey: "statsNm", - }, - { - header: "검증 항목", - accessorKey: "vrfcTrgtNm", - }, - { - header: "μ", - accessorKey: "ipcrCntVl", - }, - { - header: "Percentile", - accessorKey: "pctlCrtrVl", - }, - { - header: "A", - accessorKey: "scopAjmtCntVl", - }, - { - header: "C", - accessorKey: "thrsCntVl", - }, - { - header: "정상건수", - accessorKey: "nmlDataNocs", - }, - { - header: "제외건수", - accessorKey: "exclDataNocs", - }, - { - header: "이상건수", - accessorKey: "otlrDataNocs", - }, - ], - [targetCodeMap], + const columns = useMemo( + () => getVerificationListColumns({ rowData, checkedSns, onCheck }), + [rowData, checkedSns, onCheck], ); const table = useReactTable({ @@ -134,15 +80,13 @@ const VerificationList: React.FC = ({
)} {!isLoading && rowData.length > 0 && ( -
- -
+ )}
); diff --git a/apps/st/src/entities/statistics/model/regional-grid-columns.ts b/apps/st/src/entities/statistics/model/regional-grid-columns.ts new file mode 100644 index 000000000..5c324b398 --- /dev/null +++ b/apps/st/src/entities/statistics/model/regional-grid-columns.ts @@ -0,0 +1,83 @@ +import { ValueType } from "realgrid"; + +import type { ColumnInfo } from "@/components/type/DataGrid.type"; + +export const regionalGridColumnInfo: ColumnInfo = { + columns: [ + { + name: "sido", + fieldName: "sido", + header: { text: "시도" }, + width: 80, + }, + { + name: "gugun", + fieldName: "gugun", + header: { text: "구군" }, + width: 80, + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "행정구역" }, + width: 100, + }, + { + name: "prvyrVl", + fieldName: "prvyrVl", + header: { text: "이전년도" }, + width: 80, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "cryrVl", + fieldName: "cryrVl", + header: { text: "당해년도" }, + width: 80, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + }, + { + name: "chgRt", + fieldName: "chgRt", + header: { text: "R" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "trsfRt", + fieldName: "trsfRt", + header: { text: "S" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "otlrTrsfRt", + fieldName: "otlrTrsfRt", + header: { text: "E" }, + width: 60, + valueType: ValueType.NUMBER, + dataFormat: "#,##0.00", + }, + { + name: "otlrYn", + fieldName: "otlrYn", + header: { text: "상태" }, + width: 60, + }, + ], + columnLayout: [ + "sido", + "gugun", + "admdstNm", + "prvyrVl", + "cryrVl", + "chgRt", + "trsfRt", + "otlrTrsfRt", + "otlrYn", + ], +}; diff --git a/apps/st/src/entities/statistics/model/verification-list-columns.tsx b/apps/st/src/entities/statistics/model/verification-list-columns.tsx new file mode 100644 index 000000000..d2c503add --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-list-columns.tsx @@ -0,0 +1,108 @@ +import React from "react"; + +import type { ColumnDef } from "@tanstack/react-table"; + +import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface"; +import { Checkbox } from "@common/components"; + +interface GetVerificationListColumnsParams { + rowData: StAnlsDtlResponse[]; + checkedSns: string[]; + onCheck: (checkedSns: string[], rows: StAnlsDtlResponse[]) => void; +} + +export const getVerificationListColumns = ({ + rowData, + checkedSns, + onCheck, +}: GetVerificationListColumnsParams): ColumnDef[] => [ + { + id: "checkbox", + header: () => { + const isAllChecked = + rowData.length > 0 && checkedSns.length === rowData.length; + return ( +
+ { + if (value) { + onCheck( + rowData.map((r) => String(r.vrfcTrgtSn)), + rowData, + ); + } else { + onCheck([], []); + } + }} + aria-label="Select all rows" + /> +
+ ); + }, + cell: ({ row }) => { + const rowId = String(row.original.vrfcTrgtSn); + const isChecked = checkedSns.includes(rowId); + return ( +
+ { + const newCheckedSns = value + ? [...checkedSns, rowId] + : checkedSns.filter((id) => id !== rowId); + const newCheckedRows = rowData.filter((r) => + newCheckedSns.includes(String(r.vrfcTrgtSn)), + ); + onCheck(newCheckedSns, newCheckedRows); + }} + onClick={(e: React.MouseEvent) => e.stopPropagation()} + aria-label={`Select row ${rowId}`} + /> +
+ ); + }, + enableSorting: false, + enableHiding: false, + }, + { + header: "통계 년도", + accessorKey: "statsYr", + }, + { + header: "통계 항목", + accessorKey: "statsNm", + }, + { + header: "검증 항목", + accessorKey: "vrfcTrgtNm", + }, + { + header: "μ", + accessorKey: "ipcrCntVl", + }, + { + header: "Percentile", + accessorKey: "pctlCrtrVl", + }, + { + header: "A", + accessorKey: "scopAjmtCntVl", + }, + { + header: "C", + accessorKey: "thrsCntVl", + }, + { + header: "정상건수", + accessorKey: "nmlDataNocs", + }, + { + header: "제외건수", + accessorKey: "exclDataNocs", + }, + { + header: "이상건수", + accessorKey: "otlrDataNocs", + }, +]; From a32a1ac003e162b7c68982de5bcb400eb94fbd3a Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 17 Mar 2026 15:53:50 +0900 Subject: [PATCH 065/113] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=83=81=EC=84=B8=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20'=ED=96=89=EC=A0=95=EA=B5=AC=EC=97=AD',=20'?= =?UTF-8?q?=EC=8B=9C=EC=84=A4=EB=AA=85'=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/verification/detail/RegionalGrid.tsx | 1 + .../statistics/model/regional-grid-columns.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 1850ac456..c783ab64f 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -247,6 +247,7 @@ const RegionalGrid: React.FC = ({ 정상 이상치 + 제외
diff --git a/apps/st/src/entities/statistics/model/regional-grid-columns.ts b/apps/st/src/entities/statistics/model/regional-grid-columns.ts index 5c324b398..69eb9a316 100644 --- a/apps/st/src/entities/statistics/model/regional-grid-columns.ts +++ b/apps/st/src/entities/statistics/model/regional-grid-columns.ts @@ -17,11 +17,17 @@ export const regionalGridColumnInfo: ColumnInfo = { width: 80, }, { - name: "admdstNm", - fieldName: "admdstNm", + name: "adstDong", + fieldName: "adstDong", header: { text: "행정구역" }, width: 100, }, + { + name: "fcltNm", + fieldName: "fcltNm", + header: { text: "시설명" }, + width: 100, + }, { name: "prvyrVl", fieldName: "prvyrVl", @@ -72,7 +78,8 @@ export const regionalGridColumnInfo: ColumnInfo = { columnLayout: [ "sido", "gugun", - "admdstNm", + "adstDong", + "fcltNm", "prvyrVl", "cryrVl", "chgRt", From d320d96edfbb5b15d472c53301849b8cd64c7c7d Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 17 Mar 2026 16:05:46 +0900 Subject: [PATCH 066/113] =?UTF-8?q?[st]=20feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?"=EA=B2=80=EC=A6=9D=ED=99=95=EC=A0=95"=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/HbVerificationDetail.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx index c95b771aa..7df3c0d95 100644 --- a/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx +++ b/apps/st/src/components/statistics/verification/detail/HbVerificationDetail.tsx @@ -54,20 +54,23 @@ const HbVerificationDetail: React.FC = ({ return (
-
- -

- 검증 상세 -

+
+
+ +

+ 검증 상세 +

+
+
From 512c39cd469f2715dd2faef28108f1b2f79a061b Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 18 Mar 2026 16:57:25 +0900 Subject: [PATCH 067/113] =?UTF-8?q?[st]=20feat:=20IMR=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../st/src/entities/statistics/api/imr-api.ts | 69 +++++++++++++++++++ .../api/statistics-analysis-result-api.ts | 14 ++++ .../statistics/model/imr-interface.ts | 63 +++++++++++++++++ apps/st/src/shared/config/api.ts | 1 + 4 files changed, 147 insertions(+) create mode 100644 apps/st/src/entities/statistics/api/imr-api.ts create mode 100644 apps/st/src/entities/statistics/model/imr-interface.ts diff --git a/apps/st/src/entities/statistics/api/imr-api.ts b/apps/st/src/entities/statistics/api/imr-api.ts new file mode 100644 index 000000000..18bc8db39 --- /dev/null +++ b/apps/st/src/entities/statistics/api/imr-api.ts @@ -0,0 +1,69 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ImrDetailRequest, + ImrDetailResponse, + ImrDetectRequest, + ImrDetectResponse, + ImrListRequest, + ImrListResponse, + ImrLogRequest, + ImrLogResponse, +} from "../model/imr-interface"; + +export class ImrApiService { + /** + * 이상치 탐지 알고리즘 실행 요청 + */ + public async detectOutliers( + payload: ImrDetectRequest, + ): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.IMR}`, + payload, + ); + return response.data; + } + + /** + * 이상치 탐지 마스터 목록 조회 + */ + public async getOutliersList( + payload: ImrListRequest, + ): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.IMR}/list`, + payload, + ); + return response.data; + } + + /** + * 이상치 탐지 상세 조회 + */ + public async getOutliersDetail( + payload: ImrDetailRequest, + ): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.IMR}/list/detail`, + payload, + ); + return response.data; + } + + /** + * 이상치 산출 로그 조회 + */ + public async getOutliersLog( + payload: ImrLogRequest, + ): Promise { + const response = await client.post( + `${ENDPOINT.STATVERIFY_SERVICE.IMR}/list/detail/log`, + payload, + ); + return response.data; + } +} + +export const imrApi = new ImrApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts index e8e40f0d2..189877e42 100644 --- a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts +++ b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts @@ -21,6 +21,20 @@ export class StAnlsRsltApiService { return response.data; } + public async getImrResultList( + anlsSn: number, + params: { page?: number; size?: number }, + ): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/imr/rsltlist/${anlsSn}`, + { + params, + }, + ); + + return response.data; + } + public async getChartData( anlsSn: number, vrfcTrgtSn: number, diff --git a/apps/st/src/entities/statistics/model/imr-interface.ts b/apps/st/src/entities/statistics/model/imr-interface.ts new file mode 100644 index 000000000..148b60644 --- /dev/null +++ b/apps/st/src/entities/statistics/model/imr-interface.ts @@ -0,0 +1,63 @@ +export interface ImrDetectRequest { + anlsSn: number; + vrfcTrgtSn: number; + statsYr: string; + userId?: string; +} + +export interface ImrDetectResponse { + success: boolean; + message: string; + jobId?: string; +} + +export interface ImrListRequest { + anlsSn?: number; + vrfcTrgtSn?: number; + statsYr?: string; +} + +export interface ImrListResponse { + anlsSn: number; + vrfcTrgtSn: number; + statsYr: string; + admdstCd: string; + admdstNm: string; + modelNm: string; + anlsDt: string; +} + +export interface ImrDetailRequest { + anlsSn: number; + vrfcTrgtSn: number; + admdstCd?: string; +} + +export interface ImrDetailResponse { + anlsSn: number; + vrfcTrgtSn: number; + details: Array<{ + admdstCd: string; + admdstNm: string; + val: number; + isOutlier: boolean; + score: number; + }>; +} + +export interface ImrLogRequest { + anlsSn: number; + vrfcTrgtSn: number; + admdstCd: string; +} + +export interface ImrLogResponse { + anlsSn: number; + vrfcTrgtSn: number; + admdstCd: string; + logs: Array<{ + timestamp: string; + level: string; + message: string; + }>; +} diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts index 69cdaaf92..e00303d65 100644 --- a/apps/st/src/shared/config/api.ts +++ b/apps/st/src/shared/config/api.ts @@ -79,6 +79,7 @@ export const ENDPOINT = { ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst", ST_ANLS_DTL: "/statverify-service/api/v1/st-anls-dtl", ST_ANLS_RSLT: "/statverify-service/api/v1/st-anls-rslt", + IMR: "/statverify-service/api/v1/imr/outlier/detect", }, }; From 11cf1bf7c915062c0d92b6aedb3bacf068890d75 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 19 Mar 2026 14:15:20 +0900 Subject: [PATCH 068/113] =?UTF-8?q?st:=20feat:=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GeneralVerificationManagement.tsx | 41 +- .../generalVerification/CopyDialog.tsx | 147 +++++ .../generalVerification/DetailComponent.tsx | 394 +++++++++--- .../generalVerification/FomulaDialog.tsx | 593 ++++++++++++++++++ .../generalVerification/FormulaComponents.tsx | 41 ++ .../OperatorsComponents.tsx | 76 +++ .../generalVerification/RegistDialog.tsx | 16 +- .../management/GeneralVerificationColumn.tsx | 39 +- .../api/general-verification-service.ts | 65 +- .../model/general-verification-interface.ts | 131 +++- ...neral-verification-master-submit-schema.ts | 2 +- 11 files changed, 1419 insertions(+), 126 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx create mode 100644 apps/st/src/components/statistics/verification/generalVerification/FomulaDialog.tsx create mode 100644 apps/st/src/components/statistics/verification/generalVerification/FormulaComponents.tsx create mode 100644 apps/st/src/components/statistics/verification/generalVerification/OperatorsComponents.tsx diff --git a/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx b/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx index 7746febae..face7555d 100644 --- a/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx +++ b/apps/st/src/components/statistics/verification/GeneralVerificationManagement.tsx @@ -19,10 +19,6 @@ import DetailComponent from './generalVerification/DetailComponent'; import RegistDialog from './generalVerification/RegistDialog'; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; -import { useForm } from "react-hook-form"; -import { zodResolver } from '@hookform/resolvers/zod'; -import { generalVerificationMasterSubmitSchema, type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema'; -// import { useDataTable } from "@/shared/lib"; import { getGeneralVerificationColumns } from '@/components/util/column/management/GeneralVerificationColumn'; import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface'; import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service'; @@ -43,7 +39,7 @@ const GeneralVerificationManagement = () => { const [vrfcMasterData, setVrfcMasterData] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const [isLoading, setIsLoading] = useState(false); - const [selectedVrfcRow, setSelectedVrfcRow] = useState(null); + const [selectedVrfcRow, setSelectedVrfcRow] = useState(null); const [showRegistForm, setShowRegistForm] = useState(false); const { setMessage: alert } = useAlertStore((state) => state); @@ -86,33 +82,15 @@ const GeneralVerificationManagement = () => { data: vrfcMasterData, columns, getCoreRowModel: getCoreRowModel(), - getRowId: (row) => row.vrfcSn, + getRowId: (row) => String(row.vrfcSn), state: { rowSelection }, onRowSelectionChange: setRowSelection, }); - const form = useForm({ - resolver: zodResolver(generalVerificationMasterSubmitSchema), - defaultValues: {}, - }); - const handleVrfcMasterRowClick = async (row: GeneralverificationList) => { - try { - const response = await generalverificationApiService.getGeneralverificationInfo(row.vrfcSn); - - if (response.status === 'success') { - setRowSelection({ [row.vrfcSn]: true }); - setSelectedVrfcRow(row); - setShowRegistForm(false); - - form.reset(response.data); - } else { - alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); - form.reset({}); - } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - } + setRowSelection({ [row.vrfcSn]: true }); + setSelectedVrfcRow(row.vrfcSn); + setShowRegistForm(false); }; const handleReset = () => { @@ -122,11 +100,6 @@ const GeneralVerificationManagement = () => { setShowRegistForm(false); } - const handleClickRegist = () => { - // setRowSelection({}); - // setSelectedVrfcRow(null); - }; - useEffect(() => { handleSearch(); }, []); @@ -190,7 +163,7 @@ const GeneralVerificationManagement = () => {
- @@ -219,7 +192,7 @@ const GeneralVerificationManagement = () => { }
- +
diff --git a/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx new file mode 100644 index 000000000..b0e81c08f --- /dev/null +++ b/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx @@ -0,0 +1,147 @@ +import { useState, useEffect } from "react"; + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, + Label, + Input, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { useAlertStore } from "@/shared/lib"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { getUserInfo } from "../../../../../../../packages/common/components/src/api"; +import { LoadingWithAnimation } from "@common/components"; + +import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; + +import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface"; + +interface CopyDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + children?: React.ReactNode; + row?: GeneralverificationList; + /** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */ + onSuccess?: () => void; +} + +const CopyDialog = ({ + children, + open, + onOpenChange, + row, + onSuccess, + ...props +}: CopyDialogProps) => { + + const [selectedCode, setSelectedCode] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + + const handleChangeCode = (value: string) => { + if (row?.trgtStatsCd === value) { + alert('원본과 동일한 통계항목으로 복사할 수 없습니다.'); + return; + } + + setSelectedCode(value); + }; + + const handleCopyItem = async () => { + if (!selectedCode) { + alert('복사 대상 통계항목을 선택해주세요.'); + return; + } + + if (await confirm('복사하시겠습니까?')) { + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = await generalverificationApiService.CopyVrfcData({ + vrfcSn: row?.vrfcSn ?? null, + targetStatsCd: selectedCode ?? '', + rgtrId: userInfo.userUnqId + }); + + if (response.status === 'success') { + alert('복사되었습니다.'); + onOpenChange(false); + onSuccess?.(); + } else { + alert(response.message || '데이터 복사 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 복사 중 오류가 발생했습니다.') + } finally { + setIsLoading(false); + } + } + }; + + useEffect(() => { + if (!open) setSelectedCode(''); + }, [open]); + + return ( + + {children} + + + + {row?.vrfcNm} + + + {isLoading && +
+
+ +
+
+ } +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+ ); +} + +export default CopyDialog; \ No newline at end of file diff --git a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx index 752c6ff6b..f976488f0 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx @@ -1,12 +1,8 @@ +import { useEffect, useRef, useState } from 'react'; + import { NoData } from '@/shared/ui/no-data'; import { - // Button, - // Label, - // Select, - // SelectTrigger, - // SelectContent, - // SelectValue, - // SelectItem, + Button, Input, Table, TableBody, @@ -14,79 +10,333 @@ import { TableHead, TableRow, } from "@common/components"; -import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; +import FormulaComponents from './FormulaComponents'; +import { LoadingWithAnimation } from '@common/components'; +import FomulaDialog from './FomulaDialog'; -import { type UseFormReturn } from "react-hook-form"; +import { useAlertStore } from '@/shared/lib'; +import { useConfirm } from '@frontend-opensource/use-react-hooks'; -import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface'; -import { type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema'; +import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service'; +import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface'; +import { getUserInfo } from '../../../../../../../packages/common/components/src/api'; interface DetailComponentProps { - selectedVrfcRow: GeneralverificationList | null; - form: UseFormReturn; + selectedVrfcRow: number | null; } -const DetailComponent = ({ selectedVrfcRow, form }: DetailComponentProps) => { - return ( -
-
-

검증 상세 정보

-
-
- {selectedVrfcRow ? - <> -
-
- -
- - - - - - - - - - * 검증명 - - ( - - - - - {form.formState.errors.vrfcNm && ( -

- {form.formState.errors.vrfcNm.message} -

- )} -
- )} - /> -
- * 통계항목 - {(selectedVrfcRow as GeneralverificationList)?.trgtStatsNm ?? "-"} -
-
-
-
-
- -
- - : - +const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => { + + const [detailInfo, setDetailInfo] = useState(null); + const [formulaList, setFormulaList] = useState<(VerifyDetailResponseDto | null)[]>([]); + const [hasPersistedDetails, setHasPersistedDetails] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [showFomulaDialog, setShowFomulaDialog] = useState(false); + const [selectedFormula, setSelectedFormula] = useState(null); + const [selectedFormulaIndex, setSelectedFormulaIndex] = useState(0); + const { confirm } = useConfirm(); + + const { setMessage: alert } = useAlertStore((state) => state); + + const persistedFormulaSignatureRef = useRef(''); + + const normalizeFormulaListForCompare = (list: (VerifyDetailResponseDto | null)[]) => { + return list.map((row) => + row + ? { + vrfcDtlSn: row.vrfcDtlSn ?? null, + refStatsCd: row.refStatsCd ?? null, + kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? '', + engArtopnEqtnCn: row.engArtopnEqtnCn ?? '', + cmprOpert: row.cmprOpert ?? null, } + : null, + ); + }; + + const handleSaveClick = async () => { + const trgtStatsCd = detailInfo?.commonInfo.trgtStatsCd; + if (!trgtStatsCd) { + alert('대상 통계항목(trgtStatsCd) 정보가 없습니다.'); + return; + } + const rows = formulaList.map((row, idx) => ({ + eqtnCombSeq: idx + 1, + trgtStatsCd, + refStatsCd: row?.refStatsCd ?? null, + kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? '', + engArtopnEqtnCn: row?.engArtopnEqtnCn ?? '', + cmprOpert: row?.cmprOpert ?? null, + })); + + if (rows.length < 2) { + alert('수식 항목은 최소 2개 이상 등록해야 합니다.'); + return; + } + + const invalidIdx = rows.findIndex( + (r) => r.kornArtopnEqtnCn.trim().length === 0 || r.engArtopnEqtnCn.trim().length === 0, + ); + if (invalidIdx !== -1) { + alert(`수식이 비어있는 항목이 있습니다.\n(순번: ${invalidIdx + 1})`); + return; + } + + // 마지막을 제외한 항목들은 연산자(cmprOpert)가 반드시 있어야 함 + if (rows.length > 1) { + const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null); + if (invalidOpertIdx !== -1) { + alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`); + return; + } + } + + // 마지막 항목의 연산자는 무조건 null 처리 + if (rows.length > 0) { + const lastRow = rows[rows.length - 1]; + if (lastRow) lastRow.cmprOpert = null; + setFormulaList((prev) => { + if (prev.length === 0) return prev; + const last = prev[prev.length - 1]; + if (!last) return prev; + if (last.cmprOpert == null) return prev; + const next = [...prev]; + next[next.length - 1] = { ...last, cmprOpert: null }; + return next; + }); + } + + if (await confirm('등록하시겠습니까?')) { + try { + const userInfo = await getUserInfo(); + let response; + + if (!hasPersistedDetails) { + response = await generalverificationApiService.RegistVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, rgtrId: userInfo.userUnqId }); + } else { + response = await generalverificationApiService.UpdateVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, mdfrId: userInfo.userUnqId }); + } + + if (response?.status === 'success') { + alert('등록되었습니다.'); + getVrfcDetail(); + } else { + alert(response?.message || '데이터 등록 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 등록 중 오류가 발생했습니다.'); + } + } + }; + + const getVrfcDetail = async () => { + try { + setIsLoading(true); + + const response = await generalverificationApiService.getVrfcDetail(selectedVrfcRow!); + + if (response.status === 'success') { + setDetailInfo(response.data); + const list = response.data.verifyDetailResponseDtoList; + setFormulaList(list?.length ? [...list] : [null]); + setHasPersistedDetails((list?.length ?? 0) > 0); + // "기존 데이터" 스냅샷(서버 응답 기준)을 시그니처로 저장 + persistedFormulaSignatureRef.current = JSON.stringify( + normalizeFormulaListForCompare(list?.length ? [...list] : [null]), + ); + } else { + alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const handleClickVrfcDetail = (formula?: VerifyDetailResponseDto | null, index?: number) => { + setSelectedFormula(formula ?? null); + setSelectedFormulaIndex(index ?? 0); + setShowFomulaDialog(true); + }; + + const handleFormulaConfirm = (payload: { kornArtopnEqtnCn: string; engArtopnEqtnCn: string; refStatsCd: string | null }) => { + setFormulaList((prev) => { + const next = [...prev]; + const i = selectedFormulaIndex; + if (i < 0 || i >= next.length) return prev; + const item = next[i]; + next[i] = item + ? { + ...item, + kornArtopnEqtnCn: payload.kornArtopnEqtnCn, + engArtopnEqtnCn: payload.engArtopnEqtnCn, + refStatsCd: payload.refStatsCd, + } + : { + vrfcDtlSn: null, + refStatsCd: payload.refStatsCd, + refStatsNm: null, + kornArtopnEqtnCn: payload.kornArtopnEqtnCn, + engArtopnEqtnCn: payload.engArtopnEqtnCn, + cmprOpert: null, + }; + return next; + }); + setShowFomulaDialog(false); + setSelectedFormula(null); + }; + + const handleResetClick = async () => { + const currentSignature = JSON.stringify(normalizeFormulaListForCompare(formulaList)); + if (currentSignature === persistedFormulaSignatureRef.current) { + alert('변경된 내용이 없습니다.'); + return; + } + + if (await confirm(`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`)) { + getVrfcDetail(); + } + }; + + useEffect(() => { + if (selectedVrfcRow != null) { + getVrfcDetail(); + } + }, [selectedVrfcRow]); + + return ( + <> +
+
+

검증 상세 정보

+
+
+ {selectedVrfcRow ? + isLoading ? +
+
+ +
+
+ : +
+ + + + + + + + + + 검증명 + {detailInfo?.commonInfo.vrfcNm ?? "-"} + 통계항목 + {detailInfo?.commonInfo.trgtStatsNm ?? "-"} + + + 검증분류 + {detailInfo?.commonInfo.vrfcClsfNm ?? "-"} + 소수점처리방식 + + {detailInfo?.commonInfo.dcptDigtCnt === 0 ? + '소수점 생략' + : + `${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}` + } + + + +
+
+

수식 정보

+ {formulaList.map((data, index) => ( + { + setFormulaList((prev) => { + const next = [...prev]; + if (index < 0 || index >= next.length) return prev; + const current = next[index]; + const cmprOpert = v === 'none' ? null : v; + next[index] = current + ? { ...current, cmprOpert } + : { + vrfcDtlSn: null, + refStatsCd: null, + refStatsNm: null, + kornArtopnEqtnCn: '', + engArtopnEqtnCn: '', + cmprOpert, + }; + return next; + }); + }} + onDelete={() => { + setFormulaList((prev) => { + const next = prev.filter((_, i) => i !== index); + return next.length > 0 ? next : [null]; + }); + }} + handleFomulaDialog={(show, formula, idx) => { + if (show) handleClickVrfcDetail(formula, idx ?? index); + else setShowFomulaDialog(false); + }} + /> + ))} +
+
+ +
+
+ + +
+
+ : +
+ +
+ } +
-
+ { + setShowFomulaDialog(open); + if (!open) setSelectedFormula(null); + }} + data={selectedFormula} + trgtStatsCd={detailInfo?.commonInfo.trgtStatsCd} + trgtStatsNm={detailInfo?.commonInfo.trgtStatsNm} + onConfirm={handleFormulaConfirm} + /> + ); }; diff --git a/apps/st/src/components/statistics/verification/generalVerification/FomulaDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/FomulaDialog.tsx new file mode 100644 index 000000000..a559008d0 --- /dev/null +++ b/apps/st/src/components/statistics/verification/generalVerification/FomulaDialog.tsx @@ -0,0 +1,593 @@ +import { useEffect, useRef, useState } from "react"; + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogClose, + Textarea, + Skeleton, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; + +import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; +import type { VerifyDetailResponseDto, VerifyColumnListResponseData, SelectedVerifyColumnListResponseData } from "@/entities/statistics/model/general-verification-interface"; + +import { useAlertStore } from "@/shared/lib"; + +export interface FomulaDialogConfirmPayload { + kornArtopnEqtnCn: string; + engArtopnEqtnCn: string; + /** 참조 통계코드 (선택 안 했으면 null) */ + refStatsCd: string | null; +} + +interface FomulaDialogProps { + open: boolean; + onOpenChange?: (open: boolean) => void; + data?: VerifyDetailResponseDto | null; + trgtStatsCd?: string; + trgtStatsNm?: string; + /** 확인 클릭 시 검사 통과 후 호출. 해당 행에 반영 후 다이얼로그 닫기는 부모에서 처리 */ + onConfirm?: (payload: FomulaDialogConfirmPayload) => void; +} + +const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onConfirm }: FomulaDialogProps) => { + const [columnInfo, setColumnInfo] = useState(null); + const [selectedColumnInfo, setSelectedColumnInfo] = useState(null); + const [showCommonColumns, setShowCommonColumns] = useState(true); + const [showTargetColumns, setShowTargetColumns] = useState(true); + const [showRefColumns, setShowRefColumns] = useState(true); + /** 참조 통계 - 다이얼로그 내부에서만 사용. 열 때마다 해당 행(data)의 저장값으로 초기화, 저장 시에만 부모에 반영 예정 */ + const [refStatsCdLocal, setRefStatsCdLocal] = useState(''); + /** 수식 = 토큰 배열. 한글명 중복 시 출처(공통/대상/참조)별 engColNm 보존 */ + const [formulaDisplaySegments, setFormulaDisplaySegments] = useState([]); + const [formulaValueSegments, setFormulaValueSegments] = useState([]); + /** 확인 클릭 시 검사 실패 메시지 (Textarea 아래 빨간 글씨) */ + const [formulaSaveError, setFormulaSaveError] = useState(null); + /** 참조 통계 변경 시 안내 메시지 */ + const [refStatsChangeNotice, setRefStatsChangeNotice] = useState(null); + + const formulaTextareaRef = useRef(null); + + const { setMessage: alert } = useAlertStore((state) => state); + + /** 다이얼로그에서 쓰는 기호 → 제출용 value. 백엔드 규격에 맞게 수정 가능 */ + const OPERATOR_SYMBOL_TO_VALUE: Record = { + '+': '+', + '-': '-', + '×': '*', + '%': '/', + '(': '(', + ')': ')', + }; + + /** 공통 → 대상 → 참조 순, 첫 등장 kornColNm만 사용 (로드/직접입력 시 파싱용) */ + const getKornToEngMapForParse = (): Map => { + const map = new Map(); + const refCols = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? []; + [...(columnInfo?.commonColumnInfo ?? []), ...(columnInfo?.targetColumnInfo?.colList ?? []), ...refCols].forEach( + (item) => { + if (!map.has(item.kornColNm)) map.set(item.kornColNm, item.engColNm); + } + ); + return map; + }; + + /** 한글 수식 문자열을 토큰(표시/값) 배열로 파싱. 컬럼명은 longest match */ + const parseDisplayToSegments = (displayStr: string): { display: string[]; value: string[] } => { + if (displayStr.length === 0) return { display: [], value: [] }; + const kornToEng = getKornToEngMapForParse(); + const kornList = [...kornToEng.keys()].filter(Boolean).sort((a, b) => b.length - a.length); + const display: string[] = []; + const value: string[] = []; + let i = 0; + while (i < displayStr.length) { + let matched = false; + for (const korn of kornList) { + if (displayStr.slice(i, i + korn.length) === korn) { + display.push(korn); + value.push(kornToEng.get(korn) ?? korn); + i += korn.length; + matched = true; + break; + } + } + if (!matched) { + const ch = displayStr[i]; + if (ch != null) { + display.push(ch); + value.push(ch); + } + i += 1; + } + } + return { display, value }; + }; + + const handleFormulaKey = (val: string) => { + setFormulaSaveError(null); + if (val === 'backspace') { + setFormulaDisplaySegments((prev) => prev.slice(0, -1)); + setFormulaValueSegments((prev) => prev.slice(0, -1)); + } else if (val === 'ac') { + setFormulaDisplaySegments([]); + setFormulaValueSegments([]); + } else { + setFormulaDisplaySegments((prev) => [...prev, val]); + setFormulaValueSegments((prev) => [...prev, val]); + } + }; + + /** 컬럼 버튼: 한글 표시 + 해당 출처의 영문명 저장 (한글 중복 시에도 구분) */ + const handleFormulaColumn = (kornColNm: string, engColNm: string) => { + setFormulaSaveError(null); + setFormulaDisplaySegments((prev) => [...prev, kornColNm]); + setFormulaValueSegments((prev) => [...prev, engColNm]); + }; + + /** 제출/저장 시 사용할 영문 수식 (컬럼 engColNm + 연산자 기호 → value 치환) */ + const getFormulaValueForSubmit = (): string => + formulaValueSegments.map((seg) => OPERATOR_SYMBOL_TO_VALUE[seg] ?? seg).join(''); + + const OPERATORS = ['+', '-', '×', '/', '%'] as const; + const isOperator = (s: string) => s.length === 1 && (OPERATORS as readonly string[]).includes(s); + const isParen = (s: string) => s === '(' || s === ')'; + const isValueToken = (s: string) => + s.length > 1 || (s.length === 1 && !isOperator(s) && !isParen(s)); + const isDigit = (s: string) => s.length === 1 && /[0-9]/.test(s); + + /** 저장 시 수식 규칙 검사. 오류 시 메시지 반환, 통과 시 null */ + const validateFormulaForSave = (segments: string[]): string | null => { + if (segments.length === 0) return null; + + for (let i = 0; i < segments.length - 1; i++) { + const a = segments[i]; + const b = segments[i + 1]; + if (a != null && b != null && isValueToken(a) && isValueToken(b) && !(isDigit(a) && isDigit(b))) { + return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.'; + } + } + + // "(값)"도 하나의 값으로 취급: 값/닫는괄호 뒤에는 '(' 또는 값이 올 수 없고, 사이에 연산자가 있어야 함. + // 예: 컬럼(100), 값)(100) 같은 형태는 연산자 누락으로 간주 + for (let i = 0; i < segments.length - 1; i++) { + const a = segments[i]; + const b = segments[i + 1]; + if (a == null || b == null) continue; + // 연속 숫자(예: 50)는 예외 허용 + if (isDigit(a) && isDigit(b)) continue; + + const aIsValueBoundary = isValueToken(a) || a === ')'; + const bIsValueBoundary = b === '(' || isValueToken(b); + if (aIsValueBoundary && bIsValueBoundary) { + return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.'; + } + } + + const last = segments[segments.length - 1]; + if (last && isOperator(last)) { + return '수식은 기호로 끝날 수 없습니다. (닫는 괄호 제외)'; + } + + const stack: boolean[] = []; + for (let i = 0; i < segments.length; i++) { + const s = segments[i]; + if (s == null) continue; + if (s === '(') { + stack.push(false); + } else if (s === ')') { + if (stack.length === 0) return '닫는 괄호가 여는 괄호 없이 사용되었습니다.'; + const hadValue = stack.pop(); + if (!hadValue) return '괄호 안에 값이 있어야 합니다.'; + if (stack.length > 0) stack[stack.length - 1] = true; + } else if (stack.length > 0 && isValueToken(s)) { + stack[stack.length - 1] = true; + } + } + if (stack.length > 0) return '여는 괄호에 대응하는 닫는 괄호가 없습니다.'; + + return null; + }; + + const getVerifyColumnList = async () => { + try { + if (!trgtStatsCd) return; + let response; + + if (data?.vrfcDtlSn == null || data.vrfcDtlSn <= 0) { + response = await generalverificationApiService.getSelectedVerifyColumnList(trgtStatsCd) + } else { + response = await generalverificationApiService.getVerifyColumnList({ + trgtStatsCd, + vrfcDtlSn: data.vrfcDtlSn, + refStatsCd: data.refStatsCd, + }); + } + + + if (response?.status === 'success') { + setColumnInfo(response.data); + } else { + alert(response?.message || '데이터 조회 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 조회 중 오류가 발생했습니다.'); + } + }; + + const getSelectedVerifyColumnList = async (statsCd: string) => { + try { + const response = await generalverificationApiService.getSelectedVerifyColumnList(statsCd); + if (response?.status === 'success' && response.data) { + setSelectedColumnInfo({ + targetColumnInfo: response.data.targetColumnInfo ?? { statsCd: statsCd, colList: [] }, + commonColumnInfo: response.data.commonColumnInfo ?? [], + }); + } else { + setSelectedColumnInfo(null); + } + } catch { + setSelectedColumnInfo(null); + } + }; + + useEffect(() => { + if (open) { + setColumnInfo(null); + setSelectedColumnInfo(null); + } + if (open) void getVerifyColumnList(); + }, [open, trgtStatsCd, data?.vrfcDtlSn]); + + useEffect(() => { + if (open) setRefStatsCdLocal(data?.refStatsCd ?? ''); + }, [open, data?.refStatsCd, data?.vrfcDtlSn]); + + useEffect(() => { + if (!open) { + setSelectedColumnInfo(null); + setRefStatsCdLocal(''); + setFormulaDisplaySegments([]); + setFormulaValueSegments([]); + setFormulaSaveError(null); + setRefStatsChangeNotice(null); + } + }, [open]); + + useEffect(() => { + if (open) { + const str = data?.kornArtopnEqtnCn ?? ''; + const { display, value } = parseDisplayToSegments(str); + setFormulaDisplaySegments(display); + setFormulaValueSegments(value); + } + }, [open, data?.kornArtopnEqtnCn, data?.vrfcDtlSn]); + + /** + * 재오픈 직후에는 columnInfo가 아직 없어서 parseDisplayToSegments가 컬럼명을 매칭 못하고 + * 한글을 1글자씩 토큰화하는 경우가 있음. + * 컬럼 목록 로딩 후(= map이 채워진 후) "표시 문자열이 원본과 동일 + 값 토큰이 표시 토큰과 동일"한 케이스만 재파싱해서 + * engColNm 치환이 반영되도록 보정한다. + */ + useEffect(() => { + if (!open) return; + const src = data?.kornArtopnEqtnCn ?? ''; + if (!src) return; + + const displayJoined = formulaDisplaySegments.join(''); + const valueJoined = formulaValueSegments.join(''); + // 아직 컬럼 치환이 안 된 상태(= value가 display와 동일)에서만 재파싱 + if (displayJoined !== src) return; + if (valueJoined !== displayJoined) return; + + // 컬럼 목록이 준비된 이후에만 의미가 있음 + const hasColumns = + (columnInfo?.commonColumnInfo?.length ?? 0) > 0 || + (columnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0 || + (columnInfo?.refColumnInfo?.colList?.length ?? 0) > 0 || + (selectedColumnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0; + if (!hasColumns) return; + + const { display, value } = parseDisplayToSegments(src); + const nextDisplayJoined = display.join(''); + const nextValueJoined = value.join(''); + // 파싱 결과가 현재와 완전히 같으면 setState 반복으로 무한 렌더가 날 수 있으니 중단 + if (nextDisplayJoined === displayJoined && nextValueJoined === valueJoined) return; + // 컬럼 치환이 전혀 안 됐다면(= value가 display와 동일) 보정의 의미가 없으니 중단 + if (nextValueJoined === nextDisplayJoined) return; + + setFormulaDisplaySegments(display); + setFormulaValueSegments(value); + }, [ + open, + data?.kornArtopnEqtnCn, + columnInfo?.commonColumnInfo, + columnInfo?.targetColumnInfo?.colList, + columnInfo?.refColumnInfo?.colList, + selectedColumnInfo?.targetColumnInfo?.colList, + formulaDisplaySegments, + formulaValueSegments, + ]); + + useEffect(() => { + if (!open || columnInfo?.kornArtopnEqtnCn == null) return; + if (data?.kornArtopnEqtnCn !== columnInfo.kornArtopnEqtnCn) return; + const { display, value } = parseDisplayToSegments(columnInfo.kornArtopnEqtnCn); + setFormulaDisplaySegments(display); + setFormulaValueSegments(value); + }, [open, data?.kornArtopnEqtnCn, columnInfo?.kornArtopnEqtnCn]); + + const handleChangeRefStatsCd = (next: string) => { + if (!open) return; + + // 현재 refStatsCdLocal에 해당하는 참조 컬럼이 수식에 포함돼 있으면 변경을 막고 안내만 표시 + const currentRefCols = refColList ?? []; + const currentRefEngSet = new Set(currentRefCols.map((c) => c.engColNm).filter(Boolean)); + const hasCurrentRefToken = + currentRefEngSet.size > 0 && formulaValueSegments.some((seg) => currentRefEngSet.has(seg)); + + if (hasCurrentRefToken) { + setRefStatsChangeNotice('수식에 기존 참조 컬럼이 포함되어 있어 참조 통계를 변경할 수 없습니다. 먼저 해당 참조 컬럼을 제거해주세요.'); + return; + } + + setRefStatsChangeNotice(null); + setRefStatsCdLocal(next); + }; + + /** 참조 통계 선택 시 해당 통계의 컬럼 목록으로 교체 */ + useEffect(() => { + if (!open) return; + if (refStatsCdLocal) { + void getSelectedVerifyColumnList(refStatsCdLocal); + } else { + setSelectedColumnInfo(null); + } + }, [open, refStatsCdLocal]); + + /** 참조 컬럼: 선택한 참조 통계로 조회한 목록이 있으면 사용, 없으면 getVerifyColumnList 결과 사용 */ + const refColList = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? []; + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) { + setFormulaDisplaySegments([]); + setFormulaValueSegments([]); + setFormulaSaveError(null); + setSelectedColumnInfo(null); + setRefStatsCdLocal(''); + } + onOpenChange?.(nextOpen); + }; + + return ( + + + + + 수식 상세 정보 + + +
+
+
+

기호 및 숫자

+
+ + + + + + + {[1, 2, 3, 4, 5, 6, 7, 8, 9, 0, '⌫', 'AC'].map((n) => { + const keyVal = n !== 'AC' && n !== '⌫' ? String(n) : n === '⌫' ? 'backspace' : 'ac'; + return ( + + ); + })} +
+
+
+

수식 미리보기

+