feat: 03.하수관로 유지관리 tab1 조회기능

This commit is contained in:
2026-02-16 17:47:28 +09:00
parent 37e3b6ebdb
commit 4b883a30c5
4 changed files with 328 additions and 60 deletions
@@ -1,51 +1,159 @@
import { useState } from "react";
import { TabDataGrid } from "@rsone/components";
import { useState, useMemo, useRef } from "react";
import TabDataGrid from "@/components/module/TabDataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 관리 03. 하수관로 유지 관리
// 하수도 통계 검토 03. 하수관로 유지 관리
const SewagePipeMaintainRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('a');
const approveFunc = () => {
console.log('approveFunc');
// 승인 로직
const searchParamRef = useRef<SearchFormValues | null>(null);
const columnInfo = useMemo(() => {
return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab('a');
searchParamRef.current = null;
};
const saveFunc = () => {
console.log('submit');
// 저장 로직
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
setActiveTab('a');
setRowDataMap(new Map());
try {
setIsLoading(true);
const searchParam = {
sido: values.sido,
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: values.year,
};
searchParamRef.current = searchParam;
const response = await statisticsManagementApi.getSewerPipeMtcList(searchParam);
if (response?.status === 'success' && response.data) {
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
// API 응답이 배열을 직접 반환하는 경우와 객체로 감싸진 경우 모두 처리
const listData = Array.isArray(response.data)
? response.data
: (response.data.mtcStatusList || []);
newRowDataMap.set('a', toGridRows(listData as object[]));
setRowDataMap(newRowDataMap);
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch (error) {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
// 검토의견 작성
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const returnFunc = () => {
console.log('returnFunc');
// 반려
const existingAData = rowDataMap.get('a');
const activeTabData = rowDataMap.get(tabKey);
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
if (hasAData && !hasActiveTabData && searchParamRef.current) {
try {
setIsLoading(true);
let response;
if (tabKey === 'a') {
response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current);
}
if (response?.status === 'success' && response.data) {
setRowDataMap(prev => {
const next = new Map(prev);
// API 응답이 배열을 직접 반환하는 경우와 객체로 감싸진 경우 모두 처리
const listData = Array.isArray(response.data)
? response.data
: (response.data.mtcStatusList || []);
next.set(tabKey, toGridRows(listData as object[]));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
};
return (
<>
<SearchForm fctName={true} />
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
<ApproveButton approveFunc={() => { }} />
<ReturnButton returnFunc={() => { }} />
<SaveButton saveFunc={() => { }} />
</div>
</div>
<TabDataGrid columnInfo={SewagePipeMaintainColumnInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
</>
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
/>
</div>
);
};
@@ -1,64 +1,156 @@
import { ValueType } from "realgrid";
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type";
// 하수도 통계 관리 03. 하수관로 유지 관리
const rawMaintainList = [
{
{
tabKey: 'a',
tabLabel: '개보수',
rowStartNum: 4,
fixedColumns: 2,
columns: [
/* 1. 행정 및 기본 정보 */
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
/* 1. 행정구역 (시도) */
{
name: 'sido',
fieldName: 'sido',
header: { text: '시도' },
mergeCells: true,
editable: false,
sortdisable: true,
footer: [
{
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
const divisionIndex = 0;
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
const uniqueValues = new Set<string>();
for (let i = 0; i < rowCount; i++) {
const divisionValue = dataSource.getValue(i, 'displayYear');
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
uniqueValues.add(String(divisionValue));
}
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
}
},
{
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
const divisionIndex = 1;
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
const uniqueValues = new Set<string>();
for (let i = 0; i < rowCount; i++) {
const divisionValue = dataSource.getValue(i, 'displayYear');
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
uniqueValues.add(String(divisionValue));
}
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
}
}
]
},
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
/* 2. 구분 (displayYear) */
{
name: 'displayYear',
fieldName: 'displayYear',
header: { text: '구분' },
editable: false,
sortdisable: true,
footer: [
{
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
const divisionIndex = 0;
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
const uniqueValues = new Set<string>();
for (let i = 0; i < rowCount; i++) {
const divisionValue = dataSource.getValue(i, 'displayYear');
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
uniqueValues.add(String(divisionValue));
}
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
}
},
{
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
const divisionIndex = 1;
const dataSource = grid.getDataSource();
const rowCount = dataSource.getRowCount();
const uniqueValues = new Set<string>();
for (let i = 0; i < rowCount; i++) {
const divisionValue = dataSource.getValue(i, 'displayYear');
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
uniqueValues.add(String(divisionValue));
}
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
}
}
]
},
/* 2. 전체보수 (단위: m) */
{ name: 'RPAIR_PIPE_SM', fieldName: '계(전체보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_CMBN_PIPE', fieldName: '합류식(전체보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_RAINWTR_PIPE', fieldName: '우수(전체보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_PLTWTR_PIPE', fieldName: '오수(전체보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 전체보수 (m) - 소수점 1자리 합계 */
{ name: 'rpairPipeSm', fieldName: 'rpairPipeSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'rpairCmbnPipe', fieldName: 'rpairCmbnPipe', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'rpairRainwtrPipe', fieldName: 'rpairRainwtrPipe', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'rpairPltwtrPipe', fieldName: 'rpairPltwtrPipe', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 부분보수 (단위: 개소) */
{ name: 'RPAIR_PIPE_SM2', fieldName: '계(부분보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_CMBN_PIPE2', fieldName: '합류식(부분보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_RAINWTR_PIPE2', fieldName: '우수(부분보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RPAIR_PLTWTR_PIPE2', fieldName: '오수(부분보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 4. 부분보수 (개소) - 정수 합계 */
{ name: 'rpairPipeSm2', fieldName: 'rpairPipeSm2', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rpairCmbnPipe2', fieldName: 'rpairCmbnPipe2', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rpairRainwtrPipe2', fieldName: 'rpairRainwtrPipe2', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rpairPltwtrPipe2', fieldName: 'rpairPltwtrPipe2', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 4. 부속시설 (개소) */
{ name: 'MNHLE_SITE', fieldName: '합류식 맨홀', header: { text: '합류식\n맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PLTWTR_MNHLE_SITE', fieldName: '오수맨홀', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RAINWTR_MNHLE_SITE', fieldName: '우수맨홀', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'PLTWTR_RCVR_SITE', fieldName: '오수받이(개소)', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RAINWTR_RCVR_SITE', fieldName: '빗물받이(개소)', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SOILHOLE_SITE', fieldName: '토실(개소)', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'SOILSPLL_SITE', fieldName: '토구(개소)', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 5. 부속시설 (개소) - 정수 합계 */
{ name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '합류식\n맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'pltwtrMnhleSite', fieldName: 'pltwtrMnhleSite', header: { text: '오수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rainwtrMnhleSite', fieldName: 'rainwtrMnhleSite', header: { text: '우수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오수받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rainwtrRcvrSite', fieldName: 'rainwtrRcvrSite', header: { text: '빗물받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'soilspllSite', fieldName: 'soilspllSite', header: { text: '토실' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'soilholeSite', fieldName: 'soilholeSite', header: { text: '토구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
/* 5. 조사량 및 비고 */
{ name: 'CCTV', fieldName: '수밀검사·CCTV조사량(m)', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
/* 6. 조사량 및 비고 */
{ name: 'cctv', fieldName: 'cctv', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'rmk', fieldName: 'rmk', header: { text: '비고' } },
],
columnLayout: [
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
'YR_GBN',
{
name: 'addrGroup',
header: { text: '행정구역' },
items: [
{ column: 'sido', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] },
'gugun'
]
},
'displayYear',
{
name: 'mtcPipeGroup', header: { text: '개·보수관로' },
items: [
{
name: 'totalRepairGroup', header: { text: '전체보수(m)' },
items: ['RPAIR_PIPE_SM', 'RPAIR_CMBN_PIPE', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE', 'RPAIR_PLTWTR_PIPE'] }]
items: ['rpairPipeSm', 'rpairCmbnPipe', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['rpairRainwtrPipe', 'rpairPltwtrPipe'] }]
},
{
name: 'partRepairGroup', header: { text: '부분보수(개소)' },
items: ['RPAIR_PIPE_SM2', 'RPAIR_CMBN_PIPE2', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE2', 'RPAIR_PLTWTR_PIPE2'] }]
items: ['rpairPipeSm2', 'rpairCmbnPipe2', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['rpairRainwtrPipe2', 'rpairPltwtrPipe2'] }]
}
]
},
'MNHLE_SITE',
{ name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['PLTWTR_MNHLE_SITE', 'RAINWTR_MNHLE_SITE'] },
'PLTWTR_RCVR_SITE', 'RAINWTR_RCVR_SITE',
{ name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['SOILHOLE_SITE', 'SOILSPLL_SITE'] },
'CCTV', 'RMK'
'mnhleSite',
{ name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['pltwtrMnhleSite', 'rainwtrMnhleSite'] },
'pltwtrRcvrSite', 'rainwtrRcvrSite',
{ name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['soilspllSite', 'soilholeSite'] },
'cctv', 'rmk'
]
},
{
@@ -15,6 +15,9 @@ import {
type SewerPipeTypeApiResponse,
type SewerPipeDiamApiResponse
} from "../model/statistics-sewage-pipe-review-interface";
import {
type SewerPipeMtcApiResponse
} from "../model/statistics-sewage-pipe-mtc-review-interface";
export class StatisticsManagementApiService {
@@ -132,6 +135,25 @@ export class StatisticsManagementApiService {
);
return response.data;
}
/**
* 03. 하수관로 유지관리 개보수 현황 조회
* @param params (yr, bsnmCd, sido, gugun)
*/
public async getSewerPipeMtcList(
params: {
yr?: string;
bsnmCd?: string;
sido?: string;
gugun?: string;
}
): Promise<SewerPipeMtcApiResponse> {
const response = await axios.get<SewerPipeMtcApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe-mtc/repair`,
{ params }
);
return response.data;
}
}
// 통합 인스턴스 수출
@@ -0,0 +1,46 @@
import type { ApiResponse } from "./statistics-interface";
/**
* 03. 하수관로 유지관리 개보수 아이템
*/
export interface SewerPipeMtcItem {
// 1. 행정구역 및 기본정보
sido: string; // 시도
gugun: string; // 시군구
adstCd: string; // 행정코드
adstDong?: string; // 행정동
yr: string; // 실제 연도
displayYear: string; // 화면 표시용 (예: 2023년확정)
rowType: 'OLD' | 'CURR'; // OLD / CURR 구분
// 2. 개보수 관로 - 전체보수(m)
rpairPipeSm: number; // 계
rpairCmbnPipe: number; // 합류식
rpairRainwtrPipe: number; // 분류식-우수
rpairPltwtrPipe: number; // 분류식-오수
// 3. 개보수 관로 - 부분보수(개소)
rpairPipeSm2: number; // 계
rpairCmbnPipe2: number; // 합류식
rpairRainwtrPipe2: number; // 분류식-우수
rpairPltwtrPipe2: number; // 분류식-오수
// 4. 부속 시설물 (개소/m)
mnhleSite: number; // 합류식 맨홀
pltwtrMnhleSite: number; // 오수 맨홀 (분류식)
rainwtrMnhleSite: number; // 우수 맨홀 (분류식)
pltwtrRcvrSite: number; // 오수받이
rainwtrRcvrSite: number; // 빗물받이
soilspllSite: number; // 토실
soilholeSite: number; // 토구
cctv: number; // CCTV 조사량 (m)
// 5. 기타 및 상태값
rmk: string; // 비고
}
export interface SewerPipeMtcResponseData {
mtcStatusList: SewerPipeMtcItem[];
}
export type SewerPipeMtcApiResponse = ApiResponse<SewerPipeMtcResponseData>;