From ca83ac22aa9e70a7329b98cc849be4dd3893eaa7 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 16 Feb 2026 15:03:38 +0900 Subject: [PATCH 01/35] =?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 02/35] =?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 03/35] =?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 04/35] =?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 05/35] =?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 06/35] =?UTF-8?q?feat:=2005.=ED=8E=8C=ED=94=84=EC=9E=A5=20?= =?UTF-8?q?=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 07/35] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=9F=BC.ts-=20edit?= =?UTF-8?q?able:=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 08/35] =?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 09/35] =?UTF-8?q?st:=20feat:=20=EC=A7=84=ED=96=89=EC=83=81?= =?UTF-8?q?=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 18/35] =?UTF-8?q?st:=20fix:=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20mergeCell=20sort=20=EC=98=A4=EB=A5=98=20&=20ESLint=20?= =?UTF-8?q?=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 19/35] =?UTF-8?q?st:=20fix:=20=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20=EA=B2=80=ED=86=A0=20=EC=BB=AC=EB=9F=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20&=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=ED=8C=90=EB=B3=84=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 --- 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 20/35] =?UTF-8?q?st:=20feat:=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20=ED=95=B4=EB=8D=94=20=EC=83=89=EC=83=81=20=ED=86=B5=EC=9D=BC?= =?UTF-8?q?,=20=ED=86=B5=EA=B3=84=EC=9E=85=EB=A0=A5=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?&=20=EA=B2=80=EC=83=89=ED=8F=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95(=ED=86=B5?= =?UTF-8?q?=EA=B3=84=EC=9E=85=EB=A0=A5=ED=98=84=ED=99=A9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=8B=9C=20=EB=8F=84=EB=A5=BC=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=ED=95=9C=20=EC=8B=9C=EB=8A=94=20=ED=96=89=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=97=AD(sggNm)=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=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 21/35] =?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 22/35] =?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 23/35] =?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 24/35] =?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 25/35] =?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 26/35] =?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 27/35] =?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 29/35] =?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 30/35] =?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 31/35] =?UTF-8?q?st:=20feat:=20=EC=9D=BC=EB=B0=98=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=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 32/35] =?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 33/35] =?UTF-8?q?st:=20fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=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 34/35] =?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 35/35] =?UTF-8?q?chore:=20pre-push=20=ED=9B=85=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 --- .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",