From 1728d1879c86b6698f301ae80e8e7def766e8fd0 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 20 Mar 2026 17:51:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?st:=20fix:=20=EA=B2=80=ED=86=A0-=ED=95=98?= =?UTF-8?q?=EC=88=98=EA=B4=80=EB=A1=9C=20=EC=BB=AC=EB=9F=BC=20footer=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../total/SeweragePenetrationRateTot.tsx | 37 +- .../util/column/review/SewagePipeRvwColumn.ts | 982 +++++++++--------- .../total/SeweragePentrationRateTotColumn.ts | 215 ++++ 3 files changed, 715 insertions(+), 519 deletions(-) create mode 100644 apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts diff --git a/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx b/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx index fe266e743..be5fce614 100644 --- a/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx +++ b/apps/st/src/components/statistics/total/SeweragePenetrationRateTot.tsx @@ -1,10 +1,9 @@ +import { useState } from "react"; + +import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn"; + import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; -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 { useState } from "react"; const SeweragePenetrationRateTot = () => { const columns = [ @@ -103,38 +102,12 @@ const SeweragePenetrationRateTot = () => { 'rmk', ]; - const submitFunc = () => { - console.log('submit'); - // 제출 로직 - }; - - const saveFunc = () => { - console.log('submit'); - // 저장 로직 - }; - - const getLastYearDataFunc = () => { - console.log('get last year data'); - // 지난연도 데이터 가져오기 로직 - }; - - const SubmitExceptionFunc = () => { - console.log('submit exception'); - // 예외 제출 로직 - }; - const [rowData, setRowData] = useState[]>([]); return ( <> -
- - - - -
- + ); }; diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index 1adb4eadf..0178b60e9 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 } from "@/components/type/DataGrid.type"; const currentYear = dayjs().year() - 1; const startYear = 1996; -const totalCount = currentYear - startYear + 1 + 1; +const totalCount = currentYear - startYear + 1 + 1; // 하수도 통계 관리 02. 하수관로 const rawList = [ @@ -15,527 +15,535 @@ const rawList = [ tabLabel: '지역별 시설현황', rowStartNum: 4, fixedColumns: 2, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ + columns: [ { - 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)); + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } } - 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: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; - } - } - ] - }, - - /* 하수정비 기본계획 및 총계 */ - { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정보기본계획목표년도' } }, - { name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 합류식 */ - { name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 분류식 - 오수관로 */ - { name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 분류식 - 우수관로 */ - { name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - - /* 부속시설 */ - { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '우수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 대장 및 기타*/ - { name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, -], - columnLayout: [ - { - name: 'areaGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, - 'sggNm', - 'admdstNm', - ] - }, - 'displayYear', - 'goalYr', 'wholPplMtncPlanPrlg', 'wholPlanPrlg', 'wholFcltPrlg', 'swagePplPsplyRt', - { - name: 'combinedGroup', header: { text: '합류식(m)' }, - items: ['cbsysPplMtncPlanArea', 'cbsysPplMtncPlanPrlg', 'cbsysPlanPrlg', 'cbsysFcltPrlg', - { name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'] }, - 'cbsysOpnPrlg', 'cbsysGttrPrlg' - ], - }, - { - name: 'sepGroup', header: { text: '분류식(m)' }, - items: [ - { - name: 'foulPipeGroup', header: { text: '오수관로' }, - items: ['drtwPplMtncPlanArea', 'drtwPplMtncPlanPrlg', 'drtwPplPlanPrlg', 'drtwPplFcltPrlg', - { name: 'foulAmGroup', header: { text: '암거' }, items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'] }] - }, - { - name: 'rainPipeGroup', header: { text: '우수관로' }, - items: ['exlnPplMtncPlanArea', 'exlnPplMtncPlanPrlg', 'exlnPplPlanPrlg', 'exlnPplFcltPrlg', - { name: 'rainAmGroup', header: { text: '암거' }, items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'] }, - 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'] - } - ], - }, - 'cbsysMnhlCnt', - { name: 'fcltyGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] }, - 'drtwRcvprpCnt', 'exlnRcvprpCnt', 'stofcbrCnt', 'exlnOtflCnt', 'sewerLdgrEdpPrlg', 'sewerLdgrEdpRt', 'rmrk' - ] - }, - { - tabKey: 'ST00020002', - tabLabel: '시설별 설치현황', - rowStartNum: 4, - fixedColumns: 4, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const rowCount = dataSource.getRowCount(); - const uniqueValues = new Set(); - for (let i = 0; i < rowCount; i++) { - const divisionValue = dataSource.getValue(i, 'displayYear'); - if (divisionValue) uniqueValues.add(String(divisionValue)); - } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; - } - } - ] - }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, - { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; - } - }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; - } - } - ] - }, - { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' },valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'sprsysVartnRt', fieldName: 'sprsysVartnRt', header: { text: '분류식화' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, - { name: 'mltvEqpmCnt', fieldName: 'mltvEqpmCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'cbsysMltvEqpmCnt', fieldName: 'cbsysMltvEqpmCnt', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysMltvEqpmCnt', fieldName: 'sprsysMltvEqpmCnt', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'wholIcptgAtlfcPrlg', fieldName: 'wholIcptgAtlfcPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'icptgPrlg', fieldName: 'icptgPrlg', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'atlfcsPrlg', fieldName: 'atlfcsPrlg', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysStofcbrCnt', fieldName: 'cbsysStofcbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysStofcbrCnt', fieldName: 'sprsysStofcbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'sprsysExlnOtflCnt', fieldName: 'sprsysExlnOtflCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'wholBrchlnPrlg', fieldName: 'wholBrchlnPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'cbsysBrchlnPrlg', fieldName: 'cbsysBrchlnPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'exlnBrchlnPrlg', fieldName: 'exlnBrchlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'drtwBrchlnPrlg', fieldName: 'drtwBrchlnPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, -], - columnLayout: [ - { - name: 'addrGroup', header: { text: '행정구역' }, - items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, - 'sggNm', 'admdstNm' - ] - }, - 'swtrmtFcltNm','prcsFcltCpct', 'displayYear', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, - 'sprsysVartnRt', - { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, - { - name: 'pipeGroup', header: { text: '하수관로' }, - items: [ - 'wholFcltPrlg', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, - { - name: 'siteGroup', header: { text: '토실/토구(개소)' }, - items: [ - { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cbsysStofcbrCnt'] }, - { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysStofcbrCnt', 'sprsysExlnOtflCnt'] } ] }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { - name: 'branchGroup', header: { text: '지선(m)' }, - items: ['wholBrchlnPrlg', 'cbsysBrchlnPrlg', { name: 'sepBranchGroup', header: { text: '분류식' }, items: ['exlnBrchlnPrlg', 'drtwBrchlnPrlg'] }] - } + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : ''; + } + } + ] + }, + + /* 하수정비 기본계획 및 총계 */ + { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정보기본계획목표년도' } }, + { name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 합류식 */ + { name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 분류식 - 오수관로 */ + { name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 분류식 - 우수관로 */ + { name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + + /* 부속시설 */ + { name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '우수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + + /* 대장 및 기타*/ + { name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, ], + columnLayout: [ + { + name: 'areaGroup', header: { text: '행정구역' }, items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', + 'admdstNm', + ] + }, + 'displayYear', + 'goalYr', 'wholPplMtncPlanPrlg', 'wholPlanPrlg', 'wholFcltPrlg', 'swagePplPsplyRt', + { + name: 'combinedGroup', header: { text: '합류식(m)' }, + items: ['cbsysPplMtncPlanArea', 'cbsysPplMtncPlanPrlg', 'cbsysPlanPrlg', 'cbsysFcltPrlg', + { name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'] }, + 'cbsysOpnPrlg', 'cbsysGttrPrlg' + ], + }, + { + name: 'sepGroup', header: { text: '분류식(m)' }, + items: [ + { + name: 'foulPipeGroup', header: { text: '오수관로' }, + items: ['drtwPplMtncPlanArea', 'drtwPplMtncPlanPrlg', 'drtwPplPlanPrlg', 'drtwPplFcltPrlg', + { name: 'foulAmGroup', header: { text: '암거' }, items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'] }] + }, + { + name: 'rainPipeGroup', header: { text: '우수관로' }, + items: ['exlnPplMtncPlanArea', 'exlnPplMtncPlanPrlg', 'exlnPplPlanPrlg', 'exlnPplFcltPrlg', + { name: 'rainAmGroup', header: { text: '암거' }, items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'] }, + 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'] + } + ], + }, + 'cbsysMnhlCnt', + { name: 'fcltyGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] }, + 'drtwRcvprpCnt', 'exlnRcvprpCnt', 'stofcbrCnt', 'exlnOtflCnt', 'sewerLdgrEdpPrlg', 'sewerLdgrEdpRt', 'rmrk' + ] }, - 'rmrk' -] + + { + tabKey: 'ST00020002', + tabLabel: '시설별 설치현황', + rowStartNum: 4, + fixedColumns: 4, + columns: [ + { + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue) uniqueValues.add(String(divisionValue)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const rowCount = dataSource.getRowCount(); + const uniqueValues = new Set(); + for (let i = 0; i < rowCount; i++) { + const divisionValue = dataSource.getValue(i, 'displayYear'); + if (divisionValue) uniqueValues.add(String(divisionValue)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계'; + } + } + ] + }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, + { + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : ''; + } + } + ] + }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'sprsysVartnRt', fieldName: 'sprsysVartnRt', header: { text: '분류식화' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, + { name: 'mltvEqpmCnt', fieldName: 'mltvEqpmCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'cbsysMltvEqpmCnt', fieldName: 'cbsysMltvEqpmCnt', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysMltvEqpmCnt', fieldName: 'sprsysMltvEqpmCnt', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'wholIcptgAtlfcPrlg', fieldName: 'wholIcptgAtlfcPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'icptgPrlg', fieldName: 'icptgPrlg', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'atlfcsPrlg', fieldName: 'atlfcsPrlg', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysStofcbrCnt', fieldName: 'cbsysStofcbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysStofcbrCnt', fieldName: 'sprsysStofcbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'sprsysExlnOtflCnt', fieldName: 'sprsysExlnOtflCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, + { name: 'wholBrchlnPrlg', fieldName: 'wholBrchlnPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'cbsysBrchlnPrlg', fieldName: 'cbsysBrchlnPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'exlnBrchlnPrlg', fieldName: 'exlnBrchlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'drtwBrchlnPrlg', fieldName: 'drtwBrchlnPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], + columnLayout: [ + { + name: 'addrGroup', header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, + 'sggNm', 'admdstNm' + ] + }, + 'swtrmtFcltNm', 'prcsFcltCpct', 'displayYear', + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, + 'sprsysVartnRt', + { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, + { + name: 'pipeGroup', header: { text: '하수관로' }, + items: [ + 'wholFcltPrlg', + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, + { + name: 'siteGroup', header: { text: '토실/토구(개소)' }, + items: [ + { name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cbsysStofcbrCnt'] }, + { name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysStofcbrCnt', 'sprsysExlnOtflCnt'] } + ] + }, + { + name: 'branchGroup', header: { text: '지선(m)' }, + items: ['wholBrchlnPrlg', 'cbsysBrchlnPrlg', { name: 'sepBranchGroup', header: { text: '분류식' }, items: ['exlnBrchlnPrlg', 'drtwBrchlnPrlg'] }] + } + ], + }, + 'rmrk' + ] }, + { tabKey: 'ST00020003', tabLabel: '관종별 현황', rowStartNum: 2, fixedColumns: 2, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ + columns: [ { - 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)); + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } + ] }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - } - ] - }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; - } + ] }, - { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; - } - } + + { name: 'pipeByPrstWholSum', fieldName: 'pipeByPrstWhol', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + + ...Array.from({ length: totalCount }, (_, i) => { + const dtoNum = totalCount - i; + + const wholField = `yy${dtoNum}PrlgWhol`; + const lyngField = `yy${dtoNum}LyngPrlg`; + const dmltnField = `yy${dtoNum}DmltnPrlg`; + + return [ + { name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + ]; + }).flat(), + + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + ], columnLayout: [ + { + name: 'areaGroup', + header: { text: '행정구역' }, + items: [{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + 'sggNm', 'swrgPipeNm' + ] }, + 'displayYear', + { name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum', 'lyngPrlgSum', 'dmltnPrlgSum'] }, + + ...Array.from({ length: totalCount }, (_, i) => { + + const dtoNum = totalCount - i; + const yearLabel = i === 0 + ? '1996년이전' + : `${startYear + (i - 1)}년`; + + return { + name: `group_${yearLabel}`, + header: { text: yearLabel }, + items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`] + }; + }), + 'rmrk' ] }, - { name: 'pipeByPrstWholSum', fieldName: 'pipeByPrstWhol', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - - ...Array.from({ length: totalCount }, (_, i) => { - const dtoNum = totalCount - i; - - const wholField = `yy${dtoNum}PrlgWhol`; - const lyngField = `yy${dtoNum}LyngPrlg`; - const dmltnField = `yy${dtoNum}DmltnPrlg`; - - return [ - { name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } - ]; - }).flat(), - - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - ],columnLayout: [ - { name: 'areaGroup', header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'swrgPipeNm'] }, - 'displayYear', - { name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum','lyngPrlgSum', 'dmltnPrlgSum'] }, - - ...Array.from({ length: totalCount }, (_, i) => { - - const dtoNum = totalCount - i; - const yearLabel = i === 0 - ? '1996년이전' - : `${startYear + (i - 1)}년`; - - return { - name: `group_${yearLabel}`, - header: { text: yearLabel }, - items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`] - }; - }), - 'rmrk' - ] - }, { tabKey: 'ST00020004', tabLabel: '관경별 현황', rowStartNum: 2, fixedColumns: 3, - columns: [ - { - name: 'ctpvNm', - fieldName: 'ctpvNm', - header: { text: '시도' }, - mergeCells: true, - sortdisable: true, - footer: [ + columns: [ { - 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)); + name: 'ctpvNm', + fieldName: 'ctpvNm', + header: { text: '시도' }, + mergeCells: true, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + const divisionValues = Array.from(uniqueValues); + return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; + } } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } + ] }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, // gugun -> sggNm + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); + name: 'displayYear', + fieldName: 'displayYear', + header: { text: '구분' }, + sortdisable: true, + footer: [ + { + valueCallback: (grid: GridView) => { + const divisionIndex = 0; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } + }, + { + valueCallback: (grid: GridView) => { + const divisionIndex = 1; + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, 'displayYear'); + if (val) uniqueValues.add(String(val)); + } + return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; + } } - const divisionValues = Array.from(uniqueValues); - return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계'; - } - } - ] - }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, // gugun -> sggNm - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, - { name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true }, - { - name: 'displayYear', - fieldName: 'displayYear', - header: { text: '구분' }, - sortdisable: true, - footer: [ - { - valueCallback: (grid: GridView) => { - const divisionIndex = 0; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; - } + ] }, + + /* 관경별 수치 필드 */ + { name: 'pdmtByTsmWhol', fieldName: 'pdmtByTsmWhol', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step1PdmtPrlg', fieldName: 'step1PdmtPrlg', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step2PdmtPrlg', fieldName: 'step2PdmtPrlg', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step3PdmtPrlg', fieldName: 'step3PdmtPrlg', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step4PdmtPrlg', fieldName: 'step4PdmtPrlg', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'step5PdmtPrlg', fieldName: 'step5PdmtPrlg', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'sqrePdmtPrlg', fieldName: 'sqrePdmtPrlg', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'etcPdmtPrlg', fieldName: 'etcPdmtPrlg', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // rmk -> rmrk + ], + columnLayout: [ { - valueCallback: (grid: GridView) => { - const divisionIndex = 1; - const dataSource = grid.getDataSource(); - const uniqueValues = new Set(); - for (let i = 0; i < dataSource.getRowCount(); i++) { - const val = dataSource.getValue(i, 'displayYear'); - if (val) uniqueValues.add(String(val)); - } - return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : ''; - } - } + name: 'addrGroup', + header: { text: '행정구역' }, + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, + 'sggNm', + 'admdstNm', + ] + }, + 'swrgPipeNm', + 'displayYear', + 'pdmtByTsmWhol', + { + name: 'circleGroup', + header: { text: '원형(m)' }, + items: ['step1PdmtPrlg', 'step2PdmtPrlg', 'step3PdmtPrlg', 'step4PdmtPrlg', 'step5PdmtPrlg'] // 이름 일괄 변경 + }, + 'sqrePdmtPrlg', + 'etcPdmtPrlg', + 'rmrk' ] - }, - - /* 관경별 수치 필드 */ - { name: 'pdmtByTsmWhol', fieldName: 'pdmtByTsmWhol', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'step1PdmtPrlg', fieldName: 'step1PdmtPrlg', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'step2PdmtPrlg', fieldName: 'step2PdmtPrlg', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'step3PdmtPrlg', fieldName: 'step3PdmtPrlg', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'step4PdmtPrlg', fieldName: 'step4PdmtPrlg', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'step5PdmtPrlg', fieldName: 'step5PdmtPrlg', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'sqrePdmtPrlg', fieldName: 'sqrePdmtPrlg', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'etcPdmtPrlg', fieldName: 'etcPdmtPrlg', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // rmk -> rmrk -], - columnLayout: [ - { - name: 'addrGroup', - header: { text: '행정구역' }, - items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, - 'sggNm', - 'admdstNm', - ] - }, - 'swrgPipeNm', - 'displayYear', - 'pdmtByTsmWhol', - { - name: 'circleGroup', - header: { text: '원형(m)' }, - items: ['step1PdmtPrlg', 'step2PdmtPrlg', 'step3PdmtPrlg', 'step4PdmtPrlg', 'step5PdmtPrlg'] // 이름 일괄 변경 - }, - 'sqrePdmtPrlg', - 'etcPdmtPrlg', - 'rmrk' -] } ]; diff --git a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts new file mode 100644 index 000000000..d14c5b6c6 --- /dev/null +++ b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts @@ -0,0 +1,215 @@ +import { ValueType } from "realgrid"; +/** + * 하수도 통계 관리 > 01. 하수도 보급률 + * 단일 Tab + */ +export const SeweragePentrationRateTotColumn = { + columns: [ + // 행정구역 + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, }, + + // 수계 및 권역 + { name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } }, + + // 수계영향권별분류 + { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, }, + { name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' }, editable: false, }, + { name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, }, + + // 특별대책지역 + { name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } }, + + // 행정참고자료인구 + { name: 'pbadmsWholPpltnCnt', fieldName: 'pbadmsWholPpltnCnt', header: { text: '총인구' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', }, + { name: 'domplWholPpltnCnt', fieldName: 'domplWholPpltnCnt', header: { text: '내국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', }, + { name: 'frgnrWholPpltnCnt', fieldName: 'frgnrWholPpltnCnt', header: { text: '외국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', }, + + // 행정참고자료면적 + { name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', }, + + // 총인구 + { + name: 'wholPpltnCnt', + fieldName: 'wholPpltnCnt', + header: { text: '총인구(명)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + + // 총면적 + { + name: 'wholArea', + fieldName: 'wholArea', + header: { text: '총면적(㎢)' }, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.000', + }, + + // 하수처리구역 내 > 합계 + { + name: 'wholSwtrmtPpltnCnt', + fieldName: 'wholSwtrmtPpltnCnt', + header: { text: '합계' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + + // 하수처리구역 내 > 공공하수처리인구 + { + name: 'wholPblSwtrmtPpltnCnt', + fieldName: 'wholPblSwtrmtPpltnCnt', + header: { text: '소계' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + { name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 하수처리구역 내 > 폐수처리인구 + { + name: 'wholWwtPrcsPpltnCnt', + fieldName: 'wholWwtPrcsPpltnCnt', + header: { text: '소계' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + { name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 하수처리구역 내 > 미접속인구 + { name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 하수처리구역 내 > 면적 + { name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + + // 하수처리구역 외 + { + name: 'wholStaaExcpPpltnCnt', + fieldName: 'wholStaaExcpPpltnCnt', + header: { text: '합계' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + { name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + + // 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100) + { + name: 'staaPsplyRt', + fieldName: 'staaPsplyRt', + header: { text: '공공하수처리구역인구보급률(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + }, + + // 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100) + { + name: 'altdPrcsPsplyRt', + fieldName: 'altdPrcsPsplyRt', + header: { text: '고도처리인구보급률(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + }, + + // 하수도 설치율 + { + name: 'sewerInstlRt', + fieldName: 'sewerInstlRt', + header: { text: '하수도설치율(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + }, + + // 비고 + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, + + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false }, + ], + + columnLayout: [ + { + name: 'groupA', + hideChildHeaders: false, + header: { text: '행정구역' }, + items: [ + 'ctpvNm', + 'sggNm', + 'admdstNm', + ], + }, + 'rvbsn', + 'trbtrNm', + { + name: 'groupB', + hideChildHeaders: false, + header: { text: '수계영향권별분류' }, + items: ['mdbsn', 'lgbsn', 'mdmbsn'], + }, + 'spcCtpnRgnCn', + { + name: 'groupC', + hideChildHeaders: false, + header: { text: '행정참고자료인구' }, + items: ['pbadmsWholPpltnCnt', 'domplWholPpltnCnt', 'frgnrWholPpltnCnt'], + }, + 'refArea', + 'wholPpltnCnt', + 'wholArea', + { + name: 'groupD', + hideChildHeaders: false, + header: { text: '하수처리구역내' }, + items: [ + 'wholSwtrmtPpltnCnt', + { + name: 'pblswrgGroup', + hideChildHeaders: false, + header: { text: '공공하수처리인구(명)' }, + items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], + }, + { + name: 'wstrGroup', + hideChildHeaders: false, + header: { text: '폐수처리인구(명)' }, + items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], + }, + 'yetCntnPpltnCnt', + 'staaArea' + ], + }, + { + name: 'groupE', + hideChildHeaders: false, + header: { text: '하수처리구역외' }, + items: [ + 'wholStaaExcpPpltnCnt', + 'unprcsPpltnCnt', + 'drtwPrcsPpltnCnt', + 'spctnkPpltnCnt', + 'staaExcpArea' + ], + }, + 'staaPsplyRt', + 'altdPrcsPsplyRt', + 'sewerInstlRt', + 'rmrk', + 'swagePplPsplyRt', + 'admdstCd', + ] +}; \ No newline at end of file From a46b185c16cb477fc4d2eeb882d6838ef4847843 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 23 Mar 2026 11:45:34 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[st]=20refactor:=20=EC=8B=9C=EB=8F=84/?= =?UTF-8?q?=EC=8B=9C=EA=B5=B0=EA=B5=AC=20=EC=85=80=EB=A0=89=ED=8A=B8=20API?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/HbStatisticsVerification.tsx | 6 +- .../verification/ImrVerificationHistory.tsx | 4 +- .../verification/detail/RegionalGrid.tsx | 81 ++++++++----------- .../verification/detail/ScatterChart.tsx | 57 ++++++------- .../st/src/entities/statistics/api/queries.ts | 21 +++++ ...i.ts => statistics-analysis-master-api.ts} | 22 ++++- apps/st/src/entities/statistics/index.ts | 1 + .../statistics-analysis-master-interface.ts | 10 +++ 8 files changed, 122 insertions(+), 80 deletions(-) rename apps/st/src/entities/statistics/api/{analysis-history-api.ts => statistics-analysis-master-api.ts} (56%) diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 3bb9d22af..776e9b7b6 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -24,7 +24,7 @@ import type { } from "@/entities/statistics"; import { verificationHistoryColumns } from "@/entities/statistics"; import { analysisApi } from "@/entities/statistics/api/analysis-api"; -import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; +import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api"; import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface"; import { @@ -202,7 +202,7 @@ const HbStatisticsVerification = () => { params.verifyDtEnd = dayjs(to).format("YYYY-MM-DD"); } - return analysisHistoryApi.getAnalysisHistoryList(params); + return stAnlsMstApi.getAnalysisHistoryList(params); }, refetchInterval: (query) => { const data = query.state.data; @@ -242,7 +242,7 @@ const HbStatisticsVerification = () => { ], queryFn: async () => { if (!outlierDialog.anlsSn) return []; - const response = await analysisHistoryApi.getOutlierList( + const response = await stAnlsMstApi.getOutlierList( outlierDialog.anlsSn, ); return response; diff --git a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx index 8944b779b..583f141dc 100644 --- a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx +++ b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx @@ -33,7 +33,7 @@ import { imrDetailGridColumnInfo, imrVerificationHistoryColumns, } from "@/entities/statistics"; -import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api"; +import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api"; import { imrApi } from "@/entities/statistics/api/imr-api"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; import { @@ -130,7 +130,7 @@ const ImrVerificationHistory = () => { historyPagination, ], queryFn: () => - analysisHistoryApi.getAnalysisHistoryList({ + stAnlsMstApi.getAnalysisHistoryList({ ...appliedSearchParams, page: historyPagination.pageIndex, size: historyPagination.pageSize, diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index a932cfb3e..e1c0ab3da 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useMemo, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; - -import { ENDPOINT } from "@/shared/config"; -import { CodeApi } from "@/entities/code/api/code-service"; -import type { StAnlsRsltResponse } from "@/entities/statistics"; -import { useStAnlsRsltList } from "@/entities/statistics/api/queries"; +import type { + AdstZoneResponseDto, + StAnlsRsltResponse, +} from "@/entities/statistics"; +import { + useStAnlsRsltList, + useStAnlsSidoCombo, + useStAnlsSigunguCombo, +} from "@/entities/statistics/api/queries"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns"; import { @@ -22,6 +25,8 @@ import type { AppliedFilter } from "./HbVerificationDetail"; import DataGrid from "@/components/module/DataGrid"; +const EMPTY_ADST_LIST: AdstZoneResponseDto[] = []; + interface RegionalGridProps { anlsSn: number; vrfcTrgtSn: string | null; @@ -52,27 +57,10 @@ const RegionalGrid: React.FC = ({ Record[] >([]); - 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) return []; - - const sidoPrefix = selectedSido; - return regionCodes.filter( - (code) => - code.lwpoCd.startsWith(sidoPrefix) && code.lwpoCd.length === 7, - ); - }, [regionCodes, selectedSido]); + const { data: sidoData } = useStAnlsSidoCombo(); + const { data: gugunData } = useStAnlsSigunguCombo(selectedSido); + const sidoOptions = sidoData ?? EMPTY_ADST_LIST; + const gugunOptions = gugunData ?? EMPTY_ADST_LIST; const { data: resultData, isLoading } = useStAnlsRsltList( anlsSn, @@ -90,22 +78,22 @@ const RegionalGrid: React.FC = ({ data = showAllOnEmptyFilter ? data : []; } else { if (appliedFilter.sido) { - const sidoName = sidoOptions.find( - (opt) => opt.lwpoCd === appliedFilter.sido, - )?.lwpoCdNm; - if (sidoName) { - data = data.filter((item) => item.sido === sidoName); + const sidoCd = sidoOptions.find( + (opt) => opt.adstCd === appliedFilter.sido, + )?.adstCd; + if (sidoCd) { + data = data.filter((item) => + item.admdstCd.startsWith(sidoCd.slice(0, 2)), + ); } } if (appliedFilter.gugun) { - const gugunName = gugunOptions.find( - (opt) => opt.lwpoCd === appliedFilter.gugun, - )?.lwpoCdNm; - if (gugunName) { - data = data.filter((item) => - gugunName.includes(item.gugun), - ); + const gugunCd = gugunOptions.find( + (opt) => opt.adstCd === appliedFilter.gugun, + )?.adstCd; + if (gugunCd) { + data = data.filter((item) => item.admdstCd === gugunCd); } } @@ -135,9 +123,6 @@ const RegionalGrid: React.FC = ({ otlrYn: mapOtlrToLabel(item.otlrYnCd), })); - setGridRowData( - mappedData as unknown as Record[], - ); return mappedData; }, [ resultData, @@ -198,6 +183,10 @@ const RegionalGrid: React.FC = ({ } }; + useEffect(() => { + setGridRowData(rowData as Record[]); + }, [rowData]); + useEffect(() => { handleReset(); }, [vrfcTrgtSn]); @@ -215,8 +204,8 @@ const RegionalGrid: React.FC = ({ {sidoOptions.map((opt) => ( - - {opt.lwpoCdNm} + + {opt.adstNm} ))} @@ -231,8 +220,8 @@ const RegionalGrid: React.FC = ({ {gugunOptions.map((opt) => ( - - {opt.lwpoCdNm} + + {opt.adstNm} ))} diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 57eba1445..abba89ed1 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from "react"; import { Scatter } from "react-chartjs-2"; -import { useQuery } from "@tanstack/react-query"; import { Chart as ChartJS, type ChartData, @@ -17,8 +16,6 @@ import { } from "chart.js"; import zoomPlugin from "chartjs-plugin-zoom"; -import { ENDPOINT } from "@/shared/config"; -import { CodeApi } from "@/entities/code/api/code-service"; import type { StAnlsRsltResponse } from "@/entities/statistics"; import { useStAnlsChartData, @@ -55,29 +52,28 @@ interface DataPoint extends LinePoint { item: StAnlsRsltResponse; } +function matchesAdmdstFilter( + admdstCd: string, + sidoCd: string | null | undefined, + gugunCd: string | undefined, +): boolean { + const gugun = (gugunCd ?? "").trim(); + if (gugun) { + return admdstCd === gugun; + } + const sido = (sidoCd ?? "").trim(); + if (!sido) { + return true; + } + return admdstCd.startsWith(sido.slice(0, 2)); +} + const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, appliedFilter, onPointClick, }) => { - const { data: regionCodes } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"], - queryFn: () => CodeApi.getCode("LAESECD"), - }); - - const filterNames = useMemo(() => { - if (!regionCodes) return { sido: null, gugun: null }; - return { - sido: - regionCodes.find((c) => c.lwpoCd === appliedFilter.sido) - ?.lwpoCdNm || null, - gugun: - regionCodes.find((c) => c.lwpoCd === appliedFilter.gugun) - ?.lwpoCdNm || null, - }; - }, [regionCodes, appliedFilter.sido, appliedFilter.gugun]); - const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList( anlsSn, vrfcTrgtSn, @@ -113,12 +109,13 @@ const ScatterChart: React.FC = ({ const outlierPoints: DataPoint[] = []; rawPoints.forEach((item) => { - // Apply Filters (Sido, Gugun, Status) - if (filterNames.sido && item.sido !== filterNames.sido) { - return; - } - - if (filterNames.gugun && !filterNames.gugun.includes(item.gugun)) { + if ( + !matchesAdmdstFilter( + item.admdstCd, + appliedFilter.sido, + appliedFilter.gugun, + ) + ) { return; } @@ -249,7 +246,13 @@ const ScatterChart: React.FC = ({ }; return { data, options }; - }, [resultData, chartData, filterNames, appliedFilter.status]); + }, [ + resultData, + chartData, + appliedFilter.sido, + appliedFilter.gugun, + appliedFilter.status, + ]); return (
diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts index bd8510421..9a389bcbf 100644 --- a/apps/st/src/entities/statistics/api/queries.ts +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -2,8 +2,28 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; +import { stAnlsMstApi } from "./statistics-analysis-master-api"; import { stAnlsRsltApi } from "./statistics-analysis-result-api"; +export const useStAnlsSidoCombo = () => { + return useQuery({ + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"], + queryFn: () => stAnlsMstApi.getSidoCombo(), + }); +}; + +export const useStAnlsSigunguCombo = (sidoCd: string) => { + return useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, + "sigungu", + sidoCd, + ], + queryFn: () => stAnlsMstApi.getSigunguCombo(sidoCd), + enabled: Boolean(sidoCd), + }); +}; + export const useStAnlsRsltList = ( anlsSn: number, vrfcTrgtSn: string | null, @@ -21,6 +41,7 @@ export const useStAnlsRsltList = ( vrfcTrgtSn: Number(vrfcTrgtSn), page: 0, size: 1000, + sort: "admdstCd,asc", }), enabled: !!anlsSn && !!vrfcTrgtSn, }); diff --git a/apps/st/src/entities/statistics/api/analysis-history-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts similarity index 56% rename from apps/st/src/entities/statistics/api/analysis-history-api.ts rename to apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts index 316684f99..19a6bee1c 100644 --- a/apps/st/src/entities/statistics/api/analysis-history-api.ts +++ b/apps/st/src/entities/statistics/api/statistics-analysis-master-api.ts @@ -2,12 +2,13 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + AdstZoneResponseDto, StAnlsMstListApiResponse, StAnlsMstSearchParams, StAnlsOutlierListResponse, } from "../model/statistics-analysis/statistics-analysis-master-interface"; -export class AnalysisHistoryApiService { +export class StAnlsMstApiService { public async getAnalysisHistoryList( params: StAnlsMstSearchParams, ): Promise { @@ -30,6 +31,23 @@ export class AnalysisHistoryApiService { return response.data; } + + public async getSidoCombo(): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`, + ); + + return response.data; + } + + public async getSigunguCombo(sidoCd: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`, + { params: { sidoCd } }, + ); + + return response.data; + } } -export const analysisHistoryApi = new AnalysisHistoryApiService(); +export const stAnlsMstApi = new StAnlsMstApiService(); diff --git a/apps/st/src/entities/statistics/index.ts b/apps/st/src/entities/statistics/index.ts index 82cc2d995..44ce398c1 100644 --- a/apps/st/src/entities/statistics/index.ts +++ b/apps/st/src/entities/statistics/index.ts @@ -28,6 +28,7 @@ export { type StAnlsDtlSearchParams, } from "./model/statistics-analysis/statistics-analysis-detail-interface"; export { + type AdstZoneResponseDto, type StAnlsMstListApiResponse, type StAnlsMstResponse, type StAnlsMstResponseDto, diff --git a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts index f8a539146..046ba891b 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis/statistics-analysis-master-interface.ts @@ -1,5 +1,15 @@ import type { PageableList, PageableParams } from "@/shared/model"; +/** + * 행정구역(시도·시군구) 콤보 응답 DTO + */ +export interface AdstZoneResponseDto { + /** 행정구역 코드 */ + adstCd: string; + /** 행정구역 명 */ + adstNm: string; +} + /** * 분석 현황 마스터 응답 DTO */ From 2ee400c9bb7330437b9b270c3f64a614242d5cd6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 23 Mar 2026 14:36:18 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[st]=20feat:=20tab=20store=EC=97=90=20persi?= =?UTF-8?q?st=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/app/(with-base-layout)/layout.tsx | 251 ++++++++++-------- .../st/src/entities/auth/api/login-service.ts | 16 +- .../st/src/entities/auth/lib/token-manager.ts | 5 +- apps/st/src/store/tab-store.ts | 236 ++++++++++------ 4 files changed, 312 insertions(+), 196 deletions(-) diff --git a/apps/st/app/(with-base-layout)/layout.tsx b/apps/st/app/(with-base-layout)/layout.tsx index 98c126bf6..71343c00e 100644 --- a/apps/st/app/(with-base-layout)/layout.tsx +++ b/apps/st/app/(with-base-layout)/layout.tsx @@ -1,20 +1,24 @@ "use client"; -import { useEffect, useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { usePathname } from "next/navigation"; -import { useQueryClient, useQuery } from "@tanstack/react-query"; -import type { MenuItem } from "@/entities/menu"; -import { useMenuStore, useAlertStore } from "@/shared/lib"; -import { useTabStore, getTabStoreState } from "@/store/tab-store"; -import { useTabStore as gridTabStore } from "@rsone/components"; -import { getConfig } from "@common/config"; -import { getCookie } from "@/shared/lib"; -import { getMenuList } from "../../../../packages/common/components/src/api"; +import { + getTabStoreState as getGridTabStoreState, + useTabStore as gridTabStore, +} from "@rsone/components"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; + import { ENDPOINT } from "@/shared/config"; - -import { BaseLayout } from "@common/components"; +import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; +import type { MenuItem } from "@/entities/menu"; +import { BaseLayout } from "@common/components"; +import { getConfig } from "@common/config"; + +import { getMenuList } from "../../../../packages/common/components/src/api"; + +import { getTabStoreState, useTabStore } from "@/store/tab-store"; const appConfig = getConfig(); const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); @@ -22,118 +26,147 @@ const appSiteId = getCookie("site-id-key") ?? ""; const isLoggedIn = !!userId; export default function WithBaseLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - const pathname = usePathname(); - const queryClient = useQueryClient(); + const pathname = usePathname(); + const queryClient = useQueryClient(); - const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); - const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + const setCurrentMenuInfo = useMenuStore( + (state) => state.setCurrentMenuInfo, + ); + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); - const addTab = useTabStore((state) => state.addTab); - const setActiveTab = useTabStore((state) => state.setActiveTab); - const setMenuList = useTabStore((state) => state.setMenuList); - const activeTabId = useTabStore((state) => state.activeTabId); - const menuList = useTabStore((state) => state.menuList); - const tabs = useTabStore((state) => state.tabs); - const addTabForGrid = gridTabStore((state) => state.addTab); - const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab); - const setMenuListForGrid = gridTabStore((state) => state.setMenuList); - const alert = useAlertStore((state) => state.setMessage); + const addTab = useTabStore((state) => state.addTab); + const setActiveTab = useTabStore((state) => state.setActiveTab); + const setMenuList = useTabStore((state) => state.setMenuList); + const activeTabId = useTabStore((state) => state.activeTabId); + const menuList = useTabStore((state) => state.menuList); + const tabs = useTabStore((state) => state.tabs); + const addTabForGrid = gridTabStore((state) => state.addTab); + const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab); + const setMenuListForGrid = gridTabStore((state) => state.setMenuList); + const alert = useAlertStore((state) => state.setMessage); - const { data: menuListData } = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], - queryFn: () => getMenuList(appSiteId), - enabled: isLoggedIn, - }); + const { data: menuListData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], + queryFn: () => getMenuList(appSiteId), + enabled: isLoggedIn, + }); - useEffect(() => { - if (menuListData) { - setMenuList(menuListData); - setMenuListForGrid(menuListData); - } - }, [menuListData, setMenuList, setMenuListForGrid]); - - useEffect(() => { - if (!pathname) return; - - // pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴 - const currentTabs = getTabStoreState().tabs; - const tab = currentTabs.find((t) => t.path === pathname); - if (tab) { - setActiveTab(tab.id); - setActiveTabForGrid(tab.id); - } - }, [pathname, setActiveTab, setActiveTabForGrid]); - - // 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거 - const currentPath = useMemo(() => { - if (tabs.length === 0) return ""; - - if (activeTabId && menuList) { - // menuList에서 활성 탭의 메뉴 찾기 - const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => { - for (const item of items) { - if (item.urlPath === activeTabId) { - return item; - } - if (item.children) { - const found = findMenuByEngNm(item.children); - if (found) return found; - } + useEffect(() => { + if (menuListData) { + setMenuList(menuListData); + setMenuListForGrid(menuListData); } - return null; - }; + }, [menuListData, setMenuList, setMenuListForGrid]); - const activeMenu = findMenuByEngNm(menuList); + // 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤 + useEffect(() => { + if (!menuList) return; + const appTabs = getTabStoreState().tabs; + if (appTabs.length === 0) return; + if (getGridTabStoreState().tabs.length > 0) return; - if (activeMenu?.urlPath) { - return activeMenu.urlPath; - } - } + for (const tab of appTabs) { + getGridTabStoreState().addTab(tab.menuItem); + } - // 탭에서 직접 찾기 - if (activeTabId) { - const currentTabs = getTabStoreState().tabs; - const activeTab = currentTabs.find((t) => t.id === activeTabId); - if (activeTab?.menuItem?.urlPath) { - return activeTab.menuItem.urlPath; - } - } + const activeId = getTabStoreState().activeTabId; + if (activeId) { + const activeTab = appTabs.find((t) => t.id === activeId); + const gridActiveId = activeTab?.menuItem.menuEngNm; + if (gridActiveId) { + getGridTabStoreState().setActiveTab(gridActiveId); + } + } + }, [menuList, tabs]); - // currentMenuInfo에서 가져오기 - if (currentMenuInfo?.urlPath) { - return currentMenuInfo.urlPath; - } + useEffect(() => { + if (!pathname) return; - // 기본값으로 pathname 사용 - return pathname ?? ""; - }, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]); + // pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴 + const currentTabs = getTabStoreState().tabs; + const tab = currentTabs.find((t) => t.path === pathname); + if (tab) { + setActiveTab(tab.id); + const gridActiveId = tab.menuItem.menuEngNm; + if (gridActiveId) { + setActiveTabForGrid(gridActiveId); + } + } + }, [pathname, setActiveTab, setActiveTabForGrid]); - const handleMenuClick = useCallback((item: MenuItem) => { - setCurrentMenuInfo(item); - const addTabResult: boolean = addTab(item); - - // packages/rsone의 스토어에도 동기화 - if (addTabResult) { - addTabForGrid(item); - } + // 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거 + const currentPath = useMemo(() => { + if (tabs.length === 0) return ""; - if (!addTabResult) { - alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다. + if (activeTabId && menuList) { + // menuList에서 활성 탭의 메뉴 찾기 + const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => { + for (const item of items) { + if (item.urlPath === activeTabId) { + return item; + } + if (item.children) { + const found = findMenuByEngNm(item.children); + if (found) return found; + } + } + return null; + }; + + const activeMenu = findMenuByEngNm(menuList); + + if (activeMenu?.urlPath) { + return activeMenu.urlPath; + } + } + + // 탭에서 직접 찾기 + if (activeTabId) { + const currentTabs = getTabStoreState().tabs; + const activeTab = currentTabs.find((t) => t.id === activeTabId); + if (activeTab?.menuItem?.urlPath) { + return activeTab.menuItem.urlPath; + } + } + + // currentMenuInfo에서 가져오기 + if (currentMenuInfo?.urlPath) { + return currentMenuInfo.urlPath; + } + + // 기본값으로 pathname 사용 + return pathname ?? ""; + }, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]); + + const handleMenuClick = useCallback( + (item: MenuItem) => { + setCurrentMenuInfo(item); + const addTabResult: boolean = addTab(item); + + // packages/rsone의 스토어에도 동기화 + if (addTabResult) { + addTabForGrid(item); + } + + if (!addTabResult) { + alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다. 기존 화면을 닫은 후 이용해주세요.`); - } - }, [setCurrentMenuInfo, addTab, addTabForGrid, alert]); + } + }, + [setCurrentMenuInfo, addTab, addTabForGrid, alert], + ); - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/apps/st/src/entities/auth/api/login-service.ts b/apps/st/src/entities/auth/api/login-service.ts index c1968fe36..7fd87cc27 100644 --- a/apps/st/src/entities/auth/api/login-service.ts +++ b/apps/st/src/entities/auth/api/login-service.ts @@ -1,13 +1,18 @@ import { AxiosError } from "axios"; + import { - ENDPOINT, - JWT_EXPIRATION_TIME, ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + ENDPOINT, + JWT_EXPIRATION_TIME, } from "@/shared/config"; -import type { LoginService, ILogin } from "../model/login-interface"; +import { getCookie } from "@/shared/lib"; + import { tokenManager } from "../lib/token-manager"; +import type { ILogin,LoginService } from "../model/login-interface"; + +import { clearTabStorePersist } from "@/store/tab-store"; export class LoginApiService implements LoginService { private static instance: LoginApiService; @@ -39,6 +44,11 @@ export class LoginApiService implements LoginService { throw new Error("Missing required authentication tokens"); } + const prevTokenId = getCookie(AUTH_USER_ID); + if (prevTokenId !== undefined && prevTokenId !== tokenId) { + clearTabStorePersist(); + } + tokenManager.setToken(CLAIM_NAME, accessToken); tokenManager.setToken(AUTH_USER_ID, tokenId); tokenManager.setupRefreshTimer( diff --git a/apps/st/src/entities/auth/lib/token-manager.ts b/apps/st/src/entities/auth/lib/token-manager.ts index dd9aea352..60cd47de5 100644 --- a/apps/st/src/entities/auth/lib/token-manager.ts +++ b/apps/st/src/entities/auth/lib/token-manager.ts @@ -1,5 +1,7 @@ import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; -import { getCookie, setCookie, removeCookie } from "@/shared/lib"; +import { getCookie, removeCookie,setCookie } from "@/shared/lib"; + +import { clearTabStorePersist } from "@/store/tab-store"; export class TokenManager { private static instance: TokenManager; @@ -74,6 +76,7 @@ export class TokenManager { this.refreshTimer = null; } + clearTabStorePersist(); this.removeToken(CLAIM_NAME); this.removeToken(AUTH_USER_ID); } diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index ace855808..640cada97 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -1,8 +1,10 @@ import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + import type { MenuItem } from "@/entities/menu"; export interface Tab { - id: string; // 고유 ID (menuEngNm) + id: string; // 고유 ID (urlPath) label: string; // 탭에 표시될 이름 path: string; // 메뉴 경로 urlPath: string; // url 경로 @@ -22,96 +24,164 @@ interface TabStore { closeOtherTabs: (tabId: string) => void; } -const useTabStoreHook = create((set, get) => ({ - tabs: [], - activeTabId: null, - menuList: null, - addTab: (menuItem: MenuItem) => { - const { tabs, menuList } = get(); - const tabId = menuItem.urlPath || ''; +export const TAB_STORE_PERSIST_KEY = "st-tab-store"; +const TAB_STORE_VERSION = 1; +const MAX_TABS = 10; - // 이미 존재하는 탭인지 확인 - const existingTab = tabs.find((tab) => tab.id === tabId); +function tabForPersist(tab: Tab): Tab { + return { + ...tab, + menuItem: { ...tab.menuItem, children: null }, + }; +} - if (existingTab) { - // 이미 존재하면 활성화만 - set({ activeTabId: tabId }); - return true; - } +function sanitizePersistedTabsAndActive( + tabs: Tab[], + activeTabId: string | null, +): { tabs: Tab[]; activeTabId: string | null } { + let nextTabs = Array.isArray(tabs) ? tabs : []; + if (nextTabs.length > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS); - if (tabs.length < 10) { - const newTab: Tab = { - id: tabId, - label: menuItem.menuKornNm, - path: menuItem.menuPath, - urlPath: menuItem.urlPath, - parentEngNm: menuList?.find((m) => m.menuCd === menuItem.parentId)?.menuEngNm || '', - menuItem, - }; - - set({ - tabs: [...tabs, newTab], - activeTabId: tabId, - }); + if (nextTabs.length === 0) { + return { tabs: [], activeTabId: null }; + } - return true; - } else { - return false; - } + let nextActive = activeTabId; + if (nextActive && !nextTabs.some((t) => t.id === nextActive)) { + nextActive = nextTabs[0]?.id ?? null; + } + if (nextActive == null && nextTabs.length > 0) { + nextActive = nextTabs[0]?.id ?? null; + } - // 새 탭 추가 - }, - removeTab: (tabId: string): void => { - const { tabs, activeTabId } = get(); - const newTabs = tabs.filter((tab) => tab.id !== tabId); + return { tabs: nextTabs, activeTabId: nextActive }; +} - // 닫는 탭이 현재 활성 탭이면 다른 탭으로 전환 - let newActiveTabId = activeTabId; - if (activeTabId === tabId) { - const closedTabIndex = tabs.findIndex((tab) => tab.id === tabId); - if (newTabs.length > 0) { - // 닫힌 탭의 이전 탭을 활성화, 없으면 다음 탭 - if (closedTabIndex > 0) { - const prevTab = tabs[closedTabIndex - 1]; - newActiveTabId = prevTab ? prevTab.id : null; - } else { - const firstTab = newTabs[0]; - newActiveTabId = firstTab ? firstTab.id : null; - } - } else { - newActiveTabId = null; - } - } - - set({ - tabs: newTabs, - activeTabId: newActiveTabId, - }); - }, - setActiveTab: (tabId: string) => { - set({ activeTabId: tabId }); - }, - setMenuList: (menuList: MenuItem[] | null) => { - set({ menuList }); - }, - closeAllTabs: () => { - set({ +const useTabStoreHook = create()( + persist( + (set, get) => ({ tabs: [], activeTabId: null, - }); - }, - closeOtherTabs: (tabId: string) => { - const { tabs } = get(); - const tabToKeep = tabs.find((tab) => tab.id === tabId); + menuList: null, + addTab: (menuItem: MenuItem) => { + const { tabs, menuList } = get(); + const tabId = menuItem.urlPath || ""; - if (tabToKeep) { - set({ - tabs: [tabToKeep], - activeTabId: tabId, - }); - } - }, -})); + const existingTab = tabs.find((tab) => tab.id === tabId); + + if (existingTab) { + set({ activeTabId: tabId }); + return true; + } + + if (tabs.length < MAX_TABS) { + const newTab: Tab = { + id: tabId, + label: menuItem.menuKornNm, + path: menuItem.menuPath, + urlPath: menuItem.urlPath, + parentEngNm: + menuList?.find( + (m) => m.menuCd === menuItem.parentId, + )?.menuEngNm || "", + menuItem, + }; + + set({ + tabs: [...tabs, newTab], + activeTabId: tabId, + }); + + return true; + } + return false; + }, + removeTab: (tabId: string): void => { + const { tabs, activeTabId } = get(); + const newTabs = tabs.filter((tab) => tab.id !== tabId); + + let newActiveTabId = activeTabId; + if (activeTabId === tabId) { + const closedTabIndex = tabs.findIndex( + (tab) => tab.id === tabId, + ); + if (newTabs.length > 0) { + if (closedTabIndex > 0) { + const prevTab = tabs[closedTabIndex - 1]; + newActiveTabId = prevTab ? prevTab.id : null; + } else { + const firstTab = newTabs[0]; + newActiveTabId = firstTab ? firstTab.id : null; + } + } else { + newActiveTabId = null; + } + } + + set({ + tabs: newTabs, + activeTabId: newActiveTabId, + }); + }, + setActiveTab: (tabId: string) => { + set({ activeTabId: tabId }); + }, + setMenuList: (menuList: MenuItem[] | null) => { + set({ menuList }); + }, + closeAllTabs: () => { + set({ + tabs: [], + activeTabId: null, + }); + }, + closeOtherTabs: (tabId: string) => { + const { tabs } = get(); + const tabToKeep = tabs.find((tab) => tab.id === tabId); + + if (tabToKeep) { + set({ + tabs: [tabToKeep], + activeTabId: tabId, + }); + } + }, + }), + { + name: TAB_STORE_PERSIST_KEY, + version: TAB_STORE_VERSION, + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + tabs: state.tabs.map(tabForPersist), + activeTabId: state.activeTabId, + }), + merge: (persistedState, currentState) => { + const p = persistedState as Partial< + Pick + > | null; + const rawTabs = p?.tabs; + const rawActive = p?.activeTabId; + const { tabs, activeTabId } = sanitizePersistedTabsAndActive( + (rawTabs as Tab[]) ?? [], + typeof rawActive === "string" || rawActive === null + ? rawActive + : null, + ); + return { + ...currentState, + tabs, + activeTabId, + }; + }, + }, + ), +); export const useTabStore = useTabStoreHook; -export const getTabStoreState = () => useTabStoreHook.getState(); \ No newline at end of file +export const getTabStoreState = () => useTabStoreHook.getState(); + +// 로그아웃 또는 사용자 전환 시 로컬 탭 스냅샷 제거 및 메모리 상태 초기화 +export function clearTabStorePersist(): void { + useTabStoreHook.getState().closeAllTabs(); + useTabStoreHook.persist.clearStorage(); +} From a7c4d251a203a7fa1656507d94b349767154bf26 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 23 Mar 2026 16:44:53 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[st]=20feat:=20IMR=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verification/ImrVerificationHistory.tsx | 37 ++++++++++++++----- .../st/src/entities/statistics/api/queries.ts | 8 +--- .../api/statistics-analysis-result-api.ts | 11 ++++++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx index 583f141dc..852d21d08 100644 --- a/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx +++ b/apps/st/src/components/statistics/verification/ImrVerificationHistory.tsx @@ -90,6 +90,7 @@ const ImrVerificationHistory = () => { useState(null); const [selectedDetail, setSelectedDetail] = useState(null); + const [isDownloading, setIsDownloading] = useState(false); // Area B Grid State const [gridRowData, setGridRowData] = useState< @@ -230,6 +231,29 @@ const ImrVerificationHistory = () => { setDateRange(defaultRange); }; + const handleDownload = async () => { + if (!selectedHistory || isDownloading) return; + setIsDownloading(true); + + try { + const blob = await stAnlsRsltApi.downloadExcelImr( + selectedHistory.anlsSn, + ); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `IMR_분석결과.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error("Failed to download IMR excel", error); + } finally { + setIsDownloading(false); + } + }; + const chartOptions: ChartOptions<"line"> = useMemo( () => ({ responsive: true, @@ -442,16 +466,11 @@ const ImrVerificationHistory = () => { diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts index 9a389bcbf..0f0a9b0fb 100644 --- a/apps/st/src/entities/statistics/api/queries.ts +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -14,11 +14,7 @@ export const useStAnlsSidoCombo = () => { export const useStAnlsSigunguCombo = (sidoCd: string) => { return useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, - "sigungu", - sidoCd, - ], + queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sigungu", sidoCd], queryFn: () => stAnlsMstApi.getSigunguCombo(sidoCd), enabled: Boolean(sidoCd), }); @@ -40,7 +36,7 @@ export const useStAnlsRsltList = ( anlsSn, vrfcTrgtSn: Number(vrfcTrgtSn), page: 0, - size: 1000, + size: 15000, sort: "admdstCd,asc", }), enabled: !!anlsSn && !!vrfcTrgtSn, diff --git a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts index fa8983233..2e62fab3c 100644 --- a/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts +++ b/apps/st/src/entities/statistics/api/statistics-analysis-result-api.ts @@ -60,6 +60,17 @@ export class StAnlsRsltApiService { return response.data; } + + public async downloadExcelImr(anlsSn: number): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/excel-down/imr/${anlsSn}`, + { + responseType: "blob", + }, + ); + + return response.data; + } } export const stAnlsRsltApi = new StAnlsRsltApiService();