From 638c99ac90d93162d1f9ff911c2a9f6b27f2e312 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 7 May 2026 15:33:04 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[st]=20-=20feat:=2001.=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=ED=9B=84=20=EC=98=88=EC=99=B8=EC=82=AC=EC=9C=A0=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=20/=2002.=20=EC=9E=91=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 5 +- apps/st/src/components/module/DataGrid.tsx | 18 +- apps/st/src/components/module/TabDataGrid.tsx | 5 +- .../module/VerificationResultDialog.tsx | 82 +- .../statistics/input/SewagePipeInp.tsx | 728 +++++++++++++++++- .../input/SeweragePenetrationRateInp.tsx | 10 +- .../statistics/input/SubmitDailog.tsx | 6 +- .../management/NotEnteredStatus.tsx | 2 +- .../management/StatisticsInputManagement.tsx | 2 +- .../management/StatisticsReviewHistory.tsx | 2 +- apps/st/src/components/type/DataGrid.type.ts | 20 +- .../sewagePipe/FactInstallStatusInpColumn.ts | 87 ++- .../sewagePipe/ManholeStatusInpColumn.ts | 86 ++- .../sewagePipe/PipeDiameterStatusInpColumn.ts | 2 + .../sewagePipe/PipeTypeStatusInpColumn.ts | 85 +- .../sewagePipe/RegionalFactStatusInpColumn.ts | 174 ++++- .../util/grid/computedFieldCallbacks.ts | 102 +++ .../statistics-sewage-pipe-input-service.ts | 30 + ...cs-input-sewage-pipe-maintain-interface.ts | 19 +- .../statistics-sewage-pipe-input-interface.ts | 342 ++++---- ...tics-sewage-supply-rate-input-interface.ts | 21 +- .../statistics/model/statistics-interface.ts | 27 +- 22 files changed, 1504 insertions(+), 351 deletions(-) diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 47f6c3afd..c96f22973 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -41,7 +41,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; - if (hasFields || hasNumeratorDenominator) { + const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; + const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; + const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 82a9313d0..3ec75073f 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -292,7 +292,19 @@ function buildFieldsFromColumns( "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; - if (hasFields || hasNumeratorDenominator) { + const hasMaxOfFieldAndSum = + "baseField" in col.computed && + "sumFields" in col.computed && + col.computed.sumFields.length > 0; + const hasDifferenceOfSums = + "minuendFields" in col.computed && + "subtrahendFields" in col.computed && + col.computed.minuendFields.length > 0; + const hasSumOfMaxGroups = + "groups" in col.computed && + Array.isArray(col.computed.groups) && + col.computed.groups.length > 0; + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) { field.valueCallback = getComputedValueCallback( col.computed, ) as ( @@ -1215,6 +1227,10 @@ const DataGrid = ({ ) : 검증중} + + 반려사유:{" "} + {statusInfo.rjctOpnnCn ? statusInfo.rjctOpnnCn : "-"} + 제출일:{" "} {statusInfo.laeSbmsnYmd diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 2aba5350b..a2327c11b 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -215,7 +215,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; - if (hasFields || hasNumeratorDenominator) { + const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; + const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; + const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx index fda48ba75..b72132bbc 100644 --- a/apps/st/src/components/module/VerificationResultDialog.tsx +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -24,6 +24,7 @@ import { import DataGrid from "@/components/module/DataGrid"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import type { CellEditedCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; +import { getUserInfo } from "../../../../../packages/common/components/src/api"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; import { useAlertStore } from "@/shared/lib"; @@ -146,20 +147,15 @@ export function VerificationResultDialog({ const normalGridRef = useRef(null); const imrGridRef = useRef(null); + /** 메인 입력 화면처럼 편집 값은 그리드에 두고, 제출 시에만 스냅샷 조회 → Enter 시 rows 재주입 충돌 방지 */ + const getNormalGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); + const getImrGridDataRef = useRef<(() => VerificationResultGridRow[]) | null>(null); const alert = useAlertStore((state) => state.setMessage); const onNormalPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => { if (params.fieldName !== "prcsOpnnExpln") return; const text = String(params.newValue ?? ""); const hasValue = text.trim().length > 0; - /** DataGrid는 onCellEdited 시 React rowData를 갱신하지 않아, 제출용 상태를 여기서 맞춤 */ - setNormalRows((prev) => { - const next = [...prev]; - const row = next[params.itemIndex]; - if (!row) return prev; - next[params.itemIndex] = { ...row, prcsOpnnExpln: text }; - return next; - }); setRowCheckByItemIndex(normalGridRef, params.itemIndex, hasValue); setNormalCheckedIndices((prev) => { const next = new Set(prev); @@ -173,13 +169,6 @@ export function VerificationResultDialog({ if (params.fieldName !== "prcsOpnnExpln") return; const text = String(params.newValue ?? ""); const hasValue = text.trim().length > 0; - setImrRows((prev) => { - const next = [...prev]; - const row = next[params.itemIndex]; - if (!row) return prev; - next[params.itemIndex] = { ...row, prcsOpnnExpln: text }; - return next; - }); setRowCheckByItemIndex(imrGridRef, params.itemIndex, hasValue); setImrCheckedIndices((prev) => { const next = new Set(prev); @@ -358,25 +347,60 @@ export function VerificationResultDialog({ return () => cancelAnimationFrame(id); }, [imrRows, imrLoading]); - const handleExceptionSubmitClick = useCallback(() => { + const handleExceptionSubmitClick = useCallback(async () => { + const nvGrid = getNormalGridDataRef.current?.() ?? []; + const imGrid = getImrGridDataRef.current?.() ?? []; const payload: VerificationExceptionSubmitItem[] = []; - const pushIfHasExpln = (row: VerificationResultGridRow | undefined) => { - if (!row) return; - const prcsOpnnExpln = String(row.prcsOpnnExpln ?? "").trim(); - if (!prcsOpnnExpln) return; - payload.push({ - vrfcSn: Number(row.vrfcSn), - prcsOpnnExpln, + const pushChecked = ( + indices: Set, + gridRows: VerificationResultGridRow[], + idRows: VerificationResultGridRow[], + ) => { + indices.forEach((idx) => { + const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim(); + if (!prcsOpnnExpln) return; + const vrfcSn = Number(idRows[idx]?.vrfcSn); + if (!Number.isFinite(vrfcSn)) return; + payload.push({ vrfcSn, prcsOpnnExpln }); }); }; - normalCheckedIndices.forEach((idx) => pushIfHasExpln(normalRows[idx])); - imrCheckedIndices.forEach((idx) => pushIfHasExpln(imrRows[idx])); + pushChecked(normalCheckedIndices, nvGrid, normalRows); + pushChecked(imrCheckedIndices, imGrid, imrRows); if (payload.length === 0) { alert("작성된 예외 제출 항목이 없습니다."); return; } - console.log(payload); - }, [alert, imrCheckedIndices, imrRows, normalCheckedIndices, normalRows]); + if (!stableQueryContext) { + alert("조회 조건이 없어 예외 제출을 할 수 없습니다."); + return; + } + try { + const userInfo = await getUserInfo(); + const response = await statisticsApi.SubmitVerifyException({ + sewerBzmnCd: stableQueryContext.sewerBzmnCd, + statsYr: stableQueryContext.statsYr, + statsCd: stableQueryContext.statsCd, + userId: userInfo.userUnqId, + exceptionList: payload, + }); + if (response.status === "success") { + alert("예외 제출 처리되었습니다."); + onOpenChange(false); + } else { + alert(response.message ?? "예외 제출 처리 중 오류가 발생했습니다."); + } + } catch { + alert("예외 제출 처리 중 오류가 발생했습니다."); + } + }, [ + alert, + imrCheckedIndices, + imrRows, + loadInitial, + normalCheckedIndices, + normalRows, + stableQueryContext, + ]); const renderGrid = ( rows: VerificationResultGridRow[], @@ -386,6 +410,7 @@ export function VerificationResultDialog({ onPageIndexChange: (page: number) => void, loading: boolean, gridRef: MutableRefObject, + getGridDataRef: MutableRefObject<(() => VerificationResultGridRow[]) | null>, onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void, setCheckedIndices: Dispatch>>, ) => { @@ -404,6 +429,7 @@ export function VerificationResultDialog({ showHeaderCheckbox enableSort={false} getGridRef={gridRef} + getGridDataRef={getGridDataRef} onCellEdited={onPrcsOpnnExplnCellEdited} onItemChecked={(itemIndex, checked) => { setCheckedIndices((prev) => { @@ -467,6 +493,7 @@ export function VerificationResultDialog({ handleNormalPageChange, normalLoading, normalGridRef, + getNormalGridDataRef, onNormalPrcsOpnnExplnEdited, setNormalCheckedIndices, )} @@ -481,6 +508,7 @@ export function VerificationResultDialog({ handleImrPageChange, imrLoading, imrGridRef, + getImrGridDataRef, onImrPrcsOpnnExplnEdited, setImrCheckedIndices, )} diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index 7bd155cc7..754038519 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -10,6 +10,8 @@ import LastYearDataButton from "@/components/button/LastYearDataButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import SubmitDailog from "./SubmitDailog"; import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; @@ -26,29 +28,43 @@ import { doList } from "@/components/util/column/input/DoList"; import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; import type { SeweragePipeInputFactInstallStatusSaveRequest, + SeweragePipeInputManholeStatusItem, + SeweragePipeInputManholeStatusSaveRequest, + SeweragePipeInputPipeDiameterStatusSaveRequest, + SeweragePipeInputPipeTypeStatusItem, + SeweragePipeInputPipeTypeStatusSaveRequest, SeweragePipeInputRegionalFactStatusSaveRequest, - SeweragePipeInputStatusInfo, + SeweragePipeInputRegionalFactStatusSaveItem, SeweragePipeInputVerificationResult, SeweragePipeRequestParams, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, + SubmitExceptionRequest, +} from "@/entities/statistics/model/statistics-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import { type CheckInputPeriodItem, getVerificationPeriodActionBlockedMessage, isInputPeriodCurrent, } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; +import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn"; +import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn"; +import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn"; import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; -import type { cancelSubmitRequest } from "@/entities/statistics/model/statistics-interface"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; /** 탭별 status + 입력기간으로 편집 가능 여부 */ const computeTabEnterable = ( - statusInfo: SeweragePipeInputStatusInfo | null | undefined, + statusInfo: SewerageStatusInfo | null | undefined, periodEnterable: boolean, ): boolean => { if (statusInfo?.laeSbmsnSttsCd) { @@ -73,11 +89,192 @@ function buildSewagePipeSearchParam( }; } +const getPipeTypeLastDataYear = () => { + const lastDataYear = new Date().getFullYear() - 1; + return Math.max(1995, lastDataYear); +}; + +const getManholeLastDataYear = () => { + const lastDataYear = new Date().getFullYear() - 1; + return Math.max(2014, lastDataYear); +}; + +/** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */ +function normalizePipeTypeStatusRow( + row: SeweragePipeInputPipeTypeStatusItem, + ctpvNm: string, +): Record { + const lastDataYear = getPipeTypeLastDataYear(); + const { + yearlyStatList, + lyngPrlgWhol, + dmltnPrlgWhol, + ...rest + } = row; + const yearFields: Record = {}; + for (let year = 1995; year <= lastDataYear; year++) { + yearFields[`y${year}PrlgWhol`] = 0; + yearFields[`y${year}LyngPrlg`] = 0; + yearFields[`y${year}DmltnPrlg`] = 0; + } + + for (const y of yearlyStatList ?? []) { + const year = Number(y.lyngYr); + if (!Number.isFinite(year)) continue; + const targetYear = Math.min(lastDataYear, Math.max(1995, year)); + yearFields[`y${targetYear}PrlgWhol`] = + (yearFields[`y${targetYear}PrlgWhol`] ?? 0) + Number(y.prlgWhol ?? 0); + yearFields[`y${targetYear}LyngPrlg`] = + (yearFields[`y${targetYear}LyngPrlg`] ?? 0) + Number(y.lyngPrlg ?? 0); + yearFields[`y${targetYear}DmltnPrlg`] = + (yearFields[`y${targetYear}DmltnPrlg`] ?? 0) + Number(y.dmltnPrlg ?? 0); + } + + return { + ctpvNm, + ...rest, + lyngPrlg: Number(row.lyngPrlg ?? lyngPrlgWhol ?? 0), + dmltnPrlg: Number(row.dmltnPrlg ?? dmltnPrlgWhol ?? 0), + ...yearFields, + }; +} + +/** 맨홀 신 응답(yearlyStats) -> 그리드 yy{14..25}* 필드로 정규화 */ +function normalizeManholeStatusRow( + row: SeweragePipeInputManholeStatusItem, + ctpvNm: string, +): Record { + const maxYy = getManholeLastDataYear() % 100; + const { yearlyStats, ...rest } = row; + const yearFields: Record = {}; + for (let yy = 14; yy <= maxYy; yy++) { + yearFields[`yy${yy}WholMnhlCnt`] = 0; + yearFields[`yy${yy}Step1MnhlCnt`] = 0; + yearFields[`yy${yy}Step2MnhlCnt`] = 0; + yearFields[`yy${yy}Step3MnhlCnt`] = 0; + yearFields[`yy${yy}Step4MnhlCnt`] = 0; + yearFields[`yy${yy}Step5MnhlCnt`] = 0; + } + + for (const y of yearlyStats ?? []) { + const year = Number(y.manhYr); + if (!Number.isFinite(year)) continue; + const yy = year % 100; + if (yy < 14 || yy > maxYy) continue; + yearFields[`yy${yy}WholMnhlCnt`] = Number(y.wholMnhlCnt ?? 0); + yearFields[`yy${yy}Step1MnhlCnt`] = Number(y.step1MnhlCnt ?? 0); + yearFields[`yy${yy}Step2MnhlCnt`] = Number(y.step2MnhlCnt ?? 0); + yearFields[`yy${yy}Step3MnhlCnt`] = Number(y.step3MnhlCnt ?? 0); + yearFields[`yy${yy}Step4MnhlCnt`] = Number(y.step4MnhlCnt ?? 0); + yearFields[`yy${yy}Step5MnhlCnt`] = Number(y.step5MnhlCnt ?? 0); + } + + return { + ctpvNm, + ...rest, + ...yearFields, + }; +} + /** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */ type GridRowWithVerification = Record & { verificationResult?: SeweragePipeInputVerificationResult; }; +const REGIONAL_FACT_TAB_KEY = "ST00020001"; + +function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean { + if (!verification) return false; + return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function buildRegionalFactSubmitListFromGrid( + currentData: Record[], +): SeweragePipeInputRegionalFactStatusSaveItem[] { + const columnFormatMap = new Map(); + RegionalFactStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + return currentData.map((d) => ({ + admdstCd: String(d.admdstCd ?? ""), + sggNm: String(d.sggNm ?? ""), + admdstNm: String(d.admdstNm ?? ""), + goalYr: String(d.goalYr ?? ""), + wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), + wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), + wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), + swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"), + cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"), + cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"), + cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"), + cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"), + cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"), + cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"), + cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"), + cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"), + drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"), + drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"), + drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"), + drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"), + drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"), + drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"), + exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"), + exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"), + exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"), + exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"), + exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"), + exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"), + exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"), + exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"), + cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"), + sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"), + sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"), + drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"), + exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"), + stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"), + exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), + sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), + sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), + rmrk: (d.rmrk as string | null) ?? null, + })); +} + function tabRowDataExists( m: Map[]>, tabKey: string, @@ -112,7 +309,7 @@ function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { * @param treatAsSearched (hasRow || 이 값) 일 때 카드 후보 — load 내부에서는 searchDoneRef 반영용 */ function computeTabToolbarSnapshot( - statusInfo: SeweragePipeInputStatusInfo | null, + statusInfo: SewerageStatusInfo | null, hasRowData: boolean, treatAsSearched: boolean, searchContext: SearchFormValues | null, @@ -140,9 +337,10 @@ const SewagePipeInp = () => { const [searchValues, setSearchValues] = useState(null); const [lastSearchedYear, setLastSearchedYear] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); - const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); - // const [showDailog, setShowDailog] = useState(false); - // const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); + const [showDailog, setShowDailog] = useState(false); + const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [statsCd, setStatsCd] = useState(''); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); @@ -158,12 +356,16 @@ const SewagePipeInp = () => { const { confirm } = useConfirm(); const showGlobalProgress = useGlobalProgressStore((state) => state.show); const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const globalProgressVisible = useGlobalProgressStore((state) => state.visible); const { isAdminInfo } = useAdminStore(); const getGridDataRef = useRef<(() => Record[]) | null>(null); const getFactInstallGridDataRef = useRef<(() => Record[]) | null>(null); + const getPipeTypeGridDataRef = useRef<(() => Record[]) | null>(null); + const getPipeDiameterGridDataRef = useRef<(() => Record[]) | null>(null); + const getManholeGridDataRef = useRef<(() => Record[]) | null>(null); const searchParamRef = useRef(null); /** 검색 시 checkInputPeriod 결과 — 탭 전환 지연 조회 시 편집 가능 여부와 동일하게 맞춤 */ const periodEnterableRef = useRef(true); @@ -259,7 +461,7 @@ const SewagePipeInp = () => { const commitToolbarSnapshotForLoadedTab = ( tabKey: string, - statusInfo: SeweragePipeInputStatusInfo | null, + statusInfo: SewerageStatusInfo | null, hasRowData: boolean, searchContext: SearchFormValues, ) => { @@ -354,10 +556,9 @@ const SewagePipeInp = () => { const response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam); if (response.status === 'success') { const commonInfo = response.data.commonInfo; - const convertedData = (response.data.pipeTypeStatusList ?? []).map((d) => ({ - ctpvNm: commonInfo.ctpvNm, - ...d, - })); + const convertedData = (response.data.pipeTypeStatusList ?? []).map((d) => + normalizePipeTypeStatusRow(d, commonInfo.ctpvNm), + ); applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); @@ -402,10 +603,9 @@ const SewagePipeInp = () => { const response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam); if (response.status === 'success') { const commonInfo = response.data.commonInfo; - const convertedData = (response.data.manholeStatusList ?? []).map((d) => ({ - ctpvNm: commonInfo.ctpvNm, - ...d, - })); + const convertedData = (response.data.manholeStatusList ?? []).map((d) => + normalizeManholeStatusRow(d, commonInfo.ctpvNm), + ); applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); @@ -508,8 +708,8 @@ const SewagePipeInp = () => { const tabKeyToLoad = forcedTab ? forcedTab : yearChangedFromLastSearch - ? firstTabKey - : activeTab; + ? firstTabKey + : activeTab; if (forcedTab) { activeTabRef.current = forcedTab; @@ -587,14 +787,230 @@ const SewagePipeInp = () => { } }; - const submitFunc = () => { + const getTabGridRowsForVerification = (tabKey: string): Record[] => { + switch (tabKey) { + case "ST00020001": + return getGridDataRef.current?.() ?? []; + case "ST00020002": + return getFactInstallGridDataRef.current?.() ?? []; + case "ST00020003": + return getPipeTypeGridDataRef.current?.() ?? []; + case "ST00020004": + return getPipeDiameterGridDataRef.current?.() ?? []; + case "ST00020005": + return getManholeGridDataRef.current?.() ?? []; + case "ST00020006": + return [ + ...(rowDataMap.get("treatmentPlantConnectionInp") ?? []), + ...(rowDataMap.get("adminAreaConnectionInp") ?? []), + ]; + default: + return []; + } + }; + + const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); if (verificationBlocked) { alert(verificationBlocked); return; } - console.log('submit'); - // 제출 로직 + const userInfo = await getUserInfo(); + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? statsCd; + + const params: SubmitExceptionRequest = { + ...submitInfo, + sewerBzmnCd: searchValues?.bsnmCd ?? "", + ctpvNm: searchValues?.sido ?? "", + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""), + statsYr: searchValues?.year ?? "", + userId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + try { + setIsLoading(true); + const response = await statisticsApi.submitException(params); + if (response.status === "success") { + const values = lastSearchedValues ?? searchValues; + if (values && searchParamRef.current) { + await loadSewagePipeActiveTabData(tabKey, searchParamRef.current, values, true, false); + } + alert("제출되었습니다."); + } else { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const submitFunc = async (isException: boolean) => { + const tabKey = activeTabRef.current; + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + if (!isException) { + const currentData = getTabGridRowsForVerification(tabKey); + const rowsWithVerificationErrors = currentData.filter((d) => + hasRegionalFactVerificationErrors((d as GridRowWithVerification).verificationResult), + ); + if (rowsWithVerificationErrors.length > 0) { + alert("데이터에 오류가 있습니다.\n수정 후 제출해주세요."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setIsExceptionSubmitFlow(false); + setShowDailog(true); + } + } else { + if (await confirm("예외 제출 시 검증 결과에 오류가 있어도 제출 처리됩니다.")) { + setIsExceptionSubmitFlow(true); + setShowDailog(true); + } + } + }; + + const executeExceptionSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + await SubmitExceptionFunc(submitInfo); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); + if (verificationBlocked) { + alert(verificationBlocked); + return; + } + const tabKey = activeTabRef.current; + const values = lastSearchedValues ?? searchValues; + if (!values?.year) { + alert("검색 후 제출해 주세요."); + return; + } + const searchParam = searchParamRef.current; + if (!searchParam) { + alert("먼저 조회를 진행해주세요."); + return; + } + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? statsCd; + const statusForTab = tabStatusInfoMap.get(tabKey) ?? null; + + switch (tabKey) { + case REGIONAL_FACT_TAB_KEY: { + const currentData = getGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + + const submitList = buildRegionalFactSubmitListFromGrid(currentData); + + const requestData = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + submitList, + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + }; + + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = + await statisticsInputSewPipeApiService.submitRegionalFactStatus(requestData); + if (response.status !== "success") { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: values.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: values.year ?? "", + lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + setVerificationResultDialogOpen(true); + } catch (e) { + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await loadSewagePipeActiveTabData( + tabKey, + buildSewagePipeSearchParam(values), + values, + true, + false, + ); + alert("제출되었습니다."); + } + break; + } + default: + alert("해당 탭 제출 API는 준비 중입니다."); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } }; const cnacelSubmitFunc = async () => { @@ -614,7 +1030,7 @@ const SewagePipeInp = () => { sggNm: searchValues?.gugun ?? '', statsYr: searchValues?.year ?? '', mdfrId: userInfo.userUnqId, - statsCd: statsCd, + statsCd: tabStatsCdMap.get(activeTab) ?? statsCd, }; const response = await statisticsApi.cancelSubmit(requestData); @@ -841,6 +1257,232 @@ const SewagePipeInp = () => { } break; } + case "ST00020003": { + const currentData = getPipeTypeGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const columnFormatMap = new Map(); + PipeTypeStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + + const lastDataYear = getPipeTypeLastDataYear(); + + const saveList = currentData.map((d) => { + const yearlyStatList = []; + for (let year = 1995; year <= lastDataYear; year++) { + const wholKey = `y${year}PrlgWhol`; + const lyngKey = `y${year}LyngPrlg`; + const dmltnKey = `y${year}DmltnPrlg`; + yearlyStatList.push({ + lyngYr: String(year), + lyngPrlg: formatNumber( + d[lyngKey] as string | number | undefined, + lyngKey, + ), + dmltnPrlg: formatNumber( + d[dmltnKey] as string | number | undefined, + dmltnKey, + ), + prlgWhol: formatNumber( + d[wholKey] as string | number | undefined, + wholKey, + ), + }); + } + return { + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + pipeSeCd: String(d.pipeSeCd ?? ""), + yearlyStatList, + rmrk: String(d.rmrk ?? ""), + lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), + dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), + pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), + }; + }); + + const userInfo = await getUserInfo(); + + const param: SeweragePipeInputPipeTypeStatusSaveRequest = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? "superAdmin" : "", + saveList, + }; + + const response = await statisticsInputSewPipeApiService.savePipeTypeStatus(param); + if (response.status === "success") { + setGlobalProgress(50); + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + setGlobalProgress(100); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + break; + } + case "ST00020004": { + const currentData = getPipeDiameterGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const columnFormatMap = new Map(); + PipeDiameterStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + + const saveList = currentData.map((d) => ({ + pipeSeCd: String(d.pipeSeCd ?? ""), + step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), + step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), + step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), + step4PdmtPrlg: formatNumber(d.step4PdmtPrlg, "step4PdmtPrlg"), + step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), + sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), + etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), + rmrk: String(d.rmrk ?? ""), + pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + })); + + const userInfo = await getUserInfo(); + + const param: SeweragePipeInputPipeDiameterStatusSaveRequest = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? "superAdmin" : "", + saveList, + }; + + const response = await statisticsInputSewPipeApiService.savePipeDiameterStatus(param); + if (response.status === "success") { + setGlobalProgress(50); + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + setGlobalProgress(100); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + break; + } + case "ST00020005": { + const currentData = getManholeGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const columnFormatMap = new Map(); + ManholeStatusInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + + const minYy = 14; + const maxYy = getManholeLastDataYear() % 100; + + const saveList = currentData.map((d) => { + const yearlyStats = []; + for (let yy = minYy; yy <= maxYy; yy++) { + const year = 2000 + yy; + const wholKey = `yy${yy}WholMnhlCnt`; + const step1Key = `yy${yy}Step1MnhlCnt`; + const step2Key = `yy${yy}Step2MnhlCnt`; + const step3Key = `yy${yy}Step3MnhlCnt`; + const step4Key = `yy${yy}Step4MnhlCnt`; + const step5Key = `yy${yy}Step5MnhlCnt`; + yearlyStats.push({ + manhYr: String(year), + wholMnhlCnt: formatNumber(d[wholKey] as string | number | undefined, wholKey), + step1MnhlCnt: formatNumber(d[step1Key] as string | number | undefined, step1Key), + step2MnhlCnt: formatNumber(d[step2Key] as string | number | undefined, step2Key), + step3MnhlCnt: formatNumber(d[step3Key] as string | number | undefined, step3Key), + step4MnhlCnt: formatNumber(d[step4Key] as string | number | undefined, step4Key), + step5MnhlCnt: formatNumber(d[step5Key] as string | number | undefined, step5Key), + }); + } + return { + emphsMngSeCd: String(d.emphsMngSeCd ?? ""), + admdstCd: String(d.admdstCd ?? ""), + admdstNm: String(d.admdstNm ?? ""), + yearlyStats, + rmrk: String(d.rmrk ?? ""), + }; + }); + + const userInfo = await getUserInfo(); + const param: SeweragePipeInputManholeStatusSaveRequest = { + statsYr: values.year ?? "", + ctpvNm: values.sido ?? "", + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? "superAdmin" : "", + saveList, + }; + + const response = await statisticsInputSewPipeApiService.saveManholeStatus(param); + if (response.status === "success") { + setGlobalProgress(50); + const refreshedParam = buildSewagePipeSearchParam(values); + await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false); + setGlobalProgress(100); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + break; + } default: alert("현재 탭 저장 API 연동 전입니다."); break; @@ -896,7 +1538,7 @@ const SewagePipeInp = () => { {showEnterableToolbarButtons && ( <> void getLastYearDataFunc()} /> - + void submitFunc(false)} /> )} {activeTabStatus?.laeSbmsnSttsCd === "2" && ( @@ -1051,11 +1693,17 @@ const SewagePipeInp = () => { enableSort={false} statusInfo={tabStatusInfoMap.get(tabKey) ?? null} getGridDataRef={ - tabKey === "ST00020001" + tabKey === REGIONAL_FACT_TAB_KEY ? getGridDataRef : tabKey === "ST00020002" ? getFactInstallGridDataRef - : undefined + : tabKey === "ST00020003" + ? getPipeTypeGridDataRef + : tabKey === "ST00020004" + ? getPipeDiameterGridDataRef + : tabKey === "ST00020005" + ? getManholeGridDataRef + : undefined } cellStyleCallback={({ row, fieldName }) => { const verification = (row as GridRowWithVerification).verificationResult; @@ -1107,6 +1755,36 @@ const SewagePipeInp = () => { : null } /> + { + setShowDailog(open); + if (!open) setIsExceptionSubmitFlow(false); + }} + statusInfo={tabStatusInfoMap.get(activeTab) ?? null} + onConfirm={executeSubmit} + onExceptionConfirm={executeExceptionSubmit} + submitMode={isExceptionSubmitFlow ? "exception" : "normal"} + isLoading={isLoading} + /> + ); }; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index d79bc6f0c..ad1f4faa8 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -35,13 +35,13 @@ import { type SewerageSupplyRateSaveItem, type SewerageSupplyRateVerificationResult, type SewerageSupplyRateSaveRequest, - type SewerageSupplyRateStatusInfo, getVerificationPeriodActionBlockedMessage, isInputPeriodCurrent, } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import type { cancelSubmitRequest, ImrStatusResponse, + SewerageStatusInfo, SubmitExceptionRequest, } from "@/entities/statistics/model/statistics-interface"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; @@ -108,7 +108,7 @@ const SeweragePenetrationRateInp = () => { const [isEnterable, setIsEnterable] = useState(true); const [lastSearchedYear, setLastSearchedYear] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); - const [statusInfo, setStatusInfo] = useState(null); + const [statusInfo, setStatusInfo] = useState(null); /** 마지막 조회 시 check-input-period 응답 (검증기간 여부 판단) */ const [periodCheckList, setPeriodCheckList] = useState([]); const [showDailog, setShowDailog] = useState(false); @@ -121,6 +121,7 @@ const SeweragePenetrationRateInp = () => { const { confirm } = useConfirm(); const showGlobalProgress = useGlobalProgressStore((state) => state.show); const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const globalProgressVisible = useGlobalProgressStore((state) => state.visible); @@ -131,6 +132,7 @@ const SeweragePenetrationRateInp = () => { const activeTabId = useTabStore((s) => s.activeTabId); const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce); + useLayoutEffect(() => { if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) { return; @@ -364,7 +366,7 @@ const SeweragePenetrationRateInp = () => { return; } try { - showGlobalProgress({ label: "제출 진행 중...", progress: 0 }); + showGlobalProgress({ label: '서버에 요청 중...', progress: 0 }); const userInfo = await getUserInfo(); const currentData = getGridDataRef.current?.(); @@ -439,11 +441,13 @@ const SeweragePenetrationRateInp = () => { laeIdfrTelNo: submitInfo.laeIdfrTelNo, }; + setGlobalProgressLabel('일반 검증 진행 중...'); const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { setGlobalProgress(50); if (response.data.finalStatus === '5') { + setGlobalProgressLabel('IMR 검증 진행 중...'); const imrParam = { user_id: userInfo.userUnqId, sewer_bzmn_cd: searchValues?.bsnmCd ?? '', diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx index 856ef089c..9e0ab4e2c 100644 --- a/apps/st/src/components/statistics/input/SubmitDailog.tsx +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; -import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface"; import { Button, @@ -44,7 +44,7 @@ const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["laePicTelNo" interface SubmitDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - statusInfo: SewerageSupplyRateStatusInfo | null; + statusInfo: SewerageStatusInfo | null; /** 다이얼로그에서 저장 클릭 시 호출. 제출 정보를 넘기면 부모에서 requestData에 포함해 제출 API 호출 */ onConfirm?: (data: StatisticsInputSubmitInfoFormData) => void | Promise; /** 예외 제출로 다이얼로그를 연 경우: 일반 제출(onConfirm) 대신 호출 */ @@ -65,7 +65,7 @@ const defaultValues: StatisticsInputSubmitInfoFormData = { laeIdfrTelNo: "", }; -function statusInfoToFormData(info: SewerageSupplyRateStatusInfo): StatisticsInputSubmitInfoFormData { +function statusInfoToFormData(info: SewerageStatusInfo): StatisticsInputSubmitInfoFormData { return { laePicNm: info.laePicNm ?? "", laeKbrdrJbgdNm: info.laeKbrdrJbgdNm ?? "", diff --git a/apps/st/src/components/statistics/management/NotEnteredStatus.tsx b/apps/st/src/components/statistics/management/NotEnteredStatus.tsx index 723c29b18..20030062f 100644 --- a/apps/st/src/components/statistics/management/NotEnteredStatus.tsx +++ b/apps/st/src/components/statistics/management/NotEnteredStatus.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import type { ColumnConfig } from "@/components/type/DataGrid.type"; diff --git a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx index 150620d9b..8626b4a83 100644 --- a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx +++ b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import type { ColumnConfig } from "@/components/type/DataGrid.type"; diff --git a/apps/st/src/components/statistics/management/StatisticsReviewHistory.tsx b/apps/st/src/components/statistics/management/StatisticsReviewHistory.tsx index 96bde4c32..363aeaf2c 100644 --- a/apps/st/src/components/statistics/management/StatisticsReviewHistory.tsx +++ b/apps/st/src/components/statistics/management/StatisticsReviewHistory.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { DataGrid } from "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import type { ColumnConfig } from "@/components/type/DataGrid.type"; diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 706a7bc1d..432cf68e4 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -11,6 +11,7 @@ export type DataGridStatusBannerInfo = { laeSbmsnYmd: string | null; laePicNm: string | null; laePicTelNo: string | null; + rjctOpnnCn: string | null; }; /** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */ export type RealGridColumnInfo = { @@ -115,7 +116,24 @@ export type DataFieldValueCallback = ( /** 계산 필드 설정 (computedFieldCallbacks.ts 참고) */ export type ComputedFieldConfig = - | { fields: string[]; type: "sum" | "avg" } + | { fields: string[]; type: "sum" | "avg" | "max" | "difference" } + | { + minuendFields: string[]; + subtrahendFields: string[]; + type: "differenceOfSums"; + } + | { + baseField: string; + sumFields: string[]; + type: "maxOfFieldAndSum"; + } + | { + groups: Array<{ + baseField: string; + sumFields: string[]; + }>; + type: "sumOfMaxOfFieldAndSumGroups"; + } | { numeratorFields: string[]; denominatorFields: string[]; diff --git a/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts index 9e6b855cf..cccf7e136 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/FactInstallStatusInpColumn.ts @@ -16,25 +16,88 @@ export const FactInstallStatusInpColumn = { { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리시설용량(m³/일)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 처리구역면적(km²) - { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { + name: 'prcsZoneArea', + fieldName: 'prcsZoneArea', + header: { text: '합계' }, + footer: footerDecimal3, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.000', + computed: { fields: ['cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'], type: 'sum' as const }, + }, { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 분류식화율 - { name: 'sprsysVartnRt', fieldName: 'sprsysVartnRt', header: { text: '분류식화율' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'sprsysVartnRt', + fieldName: 'sprsysVartnRt', + header: { text: '분류식화율' }, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorFields: ['sprsysPrcsZoneArea'], + denominatorFields: ['cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 배수설비(개소) - { name: 'mltvEqpmCnt', fieldName: 'mltvEqpmCnt', header: { text: '합계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'mltvEqpmCnt', + fieldName: 'mltvEqpmCnt', + header: { text: '합계' }, + footer: footerInt, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { fields: ['sprsysMltvLnkgCnt', 'sprsysMltvYetLnkgCnt', 'cbsysMltvEqpmCnt'], type: 'sum' as const }, + }, { name: 'cbsysMltvEqpmCnt', fieldName: 'cbsysMltvEqpmCnt', header: { text: '합류식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sprsysMltvEqpmCnt', fieldName: 'sprsysMltvEqpmCnt', header: { text: '소계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'sprsysMltvEqpmCnt', + fieldName: 'sprsysMltvEqpmCnt', + header: { text: '소계' }, + footer: footerInt, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { fields: ['sprsysMltvLnkgCnt', 'sprsysMltvYetLnkgCnt'], type: 'sum' as const }, + }, { name: 'sprsysMltvLnkgCnt', fieldName: 'sprsysMltvLnkgCnt', header: { text: '연결' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'sprsysMltvYetLnkgCnt', fieldName: 'sprsysMltvYetLnkgCnt', header: { text: '미연결' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수관로 > 총시설연장 - { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'wholFcltPrlg', + fieldName: 'wholFcltPrlg', + header: { text: '총시설연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['icptgPrlg', 'atlfcsPrlg', 'cbsysBrchlnPrlg', 'exlnBrchlnPrlg', 'drtwBrchlnPrlg'], type: 'sum' as const }, + }, // 하수관로 > 차집/오수간선(m) - { name: 'wholIcptgAtlfcsPrlg', fieldName: 'wholIcptgAtlfcsPrlg', header: { text: '합계' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'wholIcptgAtlfcsPrlg', + fieldName: 'wholIcptgAtlfcsPrlg', + header: { text: '합계' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['icptgPrlg', 'atlfcsPrlg'], type: 'sum' as const }, + }, { name: 'icptgPrlg', fieldName: 'icptgPrlg', header: { text: '차집' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'atlfcsPrlg', fieldName: 'atlfcsPrlg', header: { text: '오수 간선' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, @@ -46,7 +109,17 @@ export const FactInstallStatusInpColumn = { { name: 'sprsysExlnOtflCnt', fieldName: 'sprsysExlnOtflCnt', header: { text: '우수토구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수관로 > 지선(m) - { name: 'wholBrchlnPrlg', fieldName: 'wholBrchlnPrlg', header: { text: '합계' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'wholBrchlnPrlg', + fieldName: 'wholBrchlnPrlg', + header: { text: '합계' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['cbsysBrchlnPrlg', 'exlnBrchlnPrlg', 'drtwBrchlnPrlg'], type: 'sum' as const }, + }, { name: 'cbsysBrchlnPrlg', fieldName: 'cbsysBrchlnPrlg', header: { text: '합류식' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, // 하수관로 > 지선(m) > 분류식 diff --git a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts index 8fa0b8bbb..f8f5d3a06 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/ManholeStatusInpColumn.ts @@ -4,10 +4,12 @@ import { ValueType } from "realgrid"; // 하수도 통계 관리 > 02.하수관로 > tab 5. 맨홀 현황 // SeweragePipeInputManholeStatusItem + commonInfo.ctpvNm (그리드 행에 병합) -// API: yy14=2014년 … yy25=2025년 (필드 접두 yy + 연도 끝자리) +// API: yy14=2014년 … yy{(현재연도-1)%100} (필드 접두 yy + 연도 끝자리) -const MANHOLE_YY_MIN = 14; -const MANHOLE_YY_MAX = 25; +const MANHOLE_START_YEAR = 2014; +const MANHOLE_LAST_DATA_YEAR = Math.max(MANHOLE_START_YEAR, new Date().getFullYear() - 1); +const MANHOLE_YY_MIN = MANHOLE_START_YEAR % 100; +const MANHOLE_YY_MAX = MANHOLE_LAST_DATA_YEAR % 100; const calendarYearFromYy = (yy: number) => 2000 + yy; const intCol = (name: string, headerText: string): ColumnConfig => ({ @@ -15,26 +17,73 @@ const intCol = (name: string, headerText: string): ColumnConfig => ({ fieldName: name, header: { text: headerText }, footer: footerInt, - editable: false, + // editable: false, valueType: ValueType.NUMBER, dataFormat: "#,##0", }); +const yearlySumFields = (suffix: "WholMnhlCnt" | "Step1MnhlCnt" | "Step2MnhlCnt" | "Step3MnhlCnt" | "Step4MnhlCnt" | "Step5MnhlCnt") => + Array.from({ length: MANHOLE_YY_MAX - MANHOLE_YY_MIN + 1 }, (_, i) => `yy${MANHOLE_YY_MIN + i}${suffix}`); + function totalBlockColumns(): ColumnConfig[] { return [ - intCol("totalWholMnhlCnt", "계"), - intCol("totalStep1MnhlCnt", "주철"), - intCol("totalStep2MnhlCnt", "주철(조화)"), - intCol("totalStep3MnhlCnt", "콘크리트"), - intCol("totalStep4MnhlCnt", "기타"), - intCol("totalStep5MnhlCnt", "추락방지시설설치맨홀(개)"), + { + ...intCol("totalWholMnhlCnt", "계"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("WholMnhlCnt"), type: "sum" as const }, + }, + { + ...intCol("totalStep1MnhlCnt", "주철"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("Step1MnhlCnt"), type: "sum" as const }, + }, + { + ...intCol("totalStep2MnhlCnt", "주철(조화)"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("Step2MnhlCnt"), type: "sum" as const }, + }, + { + ...intCol("totalStep3MnhlCnt", "콘크리트"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("Step3MnhlCnt"), type: "sum" as const }, + }, + { + ...intCol("totalStep4MnhlCnt", "기타"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("Step4MnhlCnt"), type: "sum" as const }, + }, + { + ...intCol("totalStep5MnhlCnt", "추락방지시설설치맨홀(개)"), + styleName: "rg-cell-total", + editable: false, + computed: { fields: yearlySumFields("Step5MnhlCnt"), type: "sum" as const }, + }, ]; } function yyBlockColumns(yy: number): ColumnConfig[] { const p = `yy${yy}`; return [ - intCol(`${p}WholMnhlCnt`, "계"), + { + ...intCol(`${p}WholMnhlCnt`, "계"), + styleName: "rg-cell-total", + editable: false, + computed: { + fields: [ + `${p}Step1MnhlCnt`, + `${p}Step2MnhlCnt`, + `${p}Step3MnhlCnt`, + `${p}Step4MnhlCnt`, + `${p}Step5MnhlCnt`, + ], + type: "sum" as const, + }, + }, intCol(`${p}Step1MnhlCnt`, "주철"), intCol(`${p}Step2MnhlCnt`, "주철(조화)"), intCol(`${p}Step3MnhlCnt`, "콘크리트"), @@ -101,8 +150,10 @@ export const ManholeStatusInpColumn = { name: "emphsMngSeNm", fieldName: "emphsMngSeNm", header: { text: "집중강우중점관리구역구분" }, - footer: { text: "합계" }, editable: false, + styleName: "rg-cell-readonly", + footer: { text: "합계" }, + // editable: false, }, ...totalBlockColumns(), ...Array.from({ length: MANHOLE_YY_MAX - MANHOLE_YY_MIN + 1 }, (_, i) => @@ -117,6 +168,14 @@ export const ManholeStatusInpColumn = { editable: false, styleName: "rg-cell-readonly", }, + { + name: "emphsMngSeCd", + fieldName: "emphsMngSeCd", + header: { text: "집중강우중점관리구역코드" }, + visible: false, + editable: false, + styleName: "rg-cell-readonly", + }, ] as ColumnConfig[], columnLayout: [ { @@ -149,7 +208,8 @@ export const ManholeStatusInpColumn = { }, ...yearLayoutItems, "rmrk", - "admdstCd" + "admdstCd", + "emphsMngSeCd", ] as ColumnLayoutItem[], rowStartNum: 3, fixedColumns: 2, diff --git a/apps/st/src/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn.ts index a852f45f2..980260008 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn.ts @@ -40,8 +40,10 @@ export const PipeDiameterStatusInpColumn = { header: { text: "총계(m)" }, footer: footerDecimal2, editable: false, + styleName: 'rg-cell-total', valueType: ValueType.NUMBER, dataFormat: "#,##0.00", + computed: { fields: ['step1PdmtPrlg', 'step2PdmtPrlg', 'step3PdmtPrlg', 'step4PdmtPrlg', 'step5PdmtPrlg', 'sqrePdmtPrlg', 'etcPdmtPrlg'], type: 'sum' as const }, }, { name: "step1PdmtPrlg", diff --git a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts index 9b61abe2a..94f943f0a 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn.ts @@ -5,23 +5,24 @@ import { ValueType } from "realgrid"; // 하수도 통계 관리 > 02.하수관로 > tab 3. 관종별 현황 // SeweragePipeInputPipeTypeStatusItem + commonInfo.ctpvNm (그리드 행에 병합) // -// 연도 그룹: yy1 = 1996년 이전, yy2 = 1996년 … API 상한 yy30(2024년)까지. -// 표시 구간: 1996년 ~ (현재 연도 − 1) 까지. API 밖 연도는 yy 인덱스 상한으로 캡. - -const YY_API_MAX = 30; -/** yy 인덱스 → 달력 연도 (yy2 → 1996) */ -const calendarYearForYyIndex = (yyIndex: number) => 1994 + yyIndex; +// 연도 그룹: y1995 = 1996년 이전, y1996 = 1996년 … +// 표시 구간: 1996년 ~ (현재 연도 − 1) 까지. /** 이 모듈이 로드될 때의 “데이터 기준 마지막 연도” (현재 연도 − 1) */ const lastDataYear = new Date().getFullYear() - 1; +const firstGroupedYear = 1995; +const firstVisibleYear = 1996; -const yyYearCols = ( - n: number, +const yearField = (year: number, suffix: "PrlgWhol" | "LyngPrlg" | "DmltnPrlg") => + `y${year}${suffix}`; + +const yearCols = ( + year: number, opts: { footer?: typeof footerDecimal2; visible?: boolean } = {} ): ColumnConfig[] => { - const whol = `yy${n}PrlgWhol`; - const lyng = `yy${n}LyngPrlg`; - const dmltn = `yy${n}DmltnPrlg`; + const whol = yearField(year, "PrlgWhol"); + const lyng = yearField(year, "LyngPrlg"); + const dmltn = yearField(year, "DmltnPrlg"); const vis = opts.visible === false ? ({ visible: false } as const) : {}; return [ { @@ -30,8 +31,10 @@ const yyYearCols = ( header: { text: "계" }, ...(opts.footer !== undefined ? { footer: opts.footer } : {}), editable: false, + styleName: "rg-cell-total", valueType: ValueType.NUMBER, dataFormat: "#,##0.00", + computed: { fields: [lyng, dmltn], type: "difference" as const }, ...vis, }, { @@ -39,6 +42,8 @@ const yyYearCols = ( fieldName: lyng, header: { text: "매설연장(m)" }, ...(opts.footer !== undefined ? { footer: opts.footer } : {}), + editable: false, + styleName: "rg-cell-readonly", valueType: ValueType.NUMBER, dataFormat: "#,##0.00", ...vis, @@ -55,18 +60,25 @@ const yyYearCols = ( ]; }; -/** yy2(1996) ~ yyK 까지 표시. K는 API 상한 YY_API_MAX */ -const maxYyIndex = - lastDataYear < 1996 ? 1 : Math.min(YY_API_MAX, lastDataYear - 1994); +const visibleYears = + lastDataYear < firstVisibleYear + ? [] + : Array.from( + { length: lastDataYear - firstVisibleYear + 1 }, + (_, i) => firstVisibleYear + i, + ); const yearLayoutItems: ColumnLayoutItem[] = []; -for (let yy = 2; yy <= maxYyIndex; yy++) { - const y = calendarYearForYyIndex(yy); +for (const y of visibleYears) { yearLayoutItems.push({ name: `year${y}`, hideChildHeaders: false, header: { text: `${y}년` }, - items: [`yy${yy}PrlgWhol`, `yy${yy}LyngPrlg`, `yy${yy}DmltnPrlg`], + items: [ + yearField(y, "PrlgWhol"), + yearField(y, "LyngPrlg"), + yearField(y, "DmltnPrlg"), + ], }); } @@ -105,15 +117,28 @@ export const PipeTypeStatusInpColumn = { name: "pipeByPrstWhol", fieldName: "pipeByPrstWhol", header: { text: "계" }, + styleName: "rg-cell-total", footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", + computed: { + minuendFields: [ + yearField(firstGroupedYear, "LyngPrlg"), + ...visibleYears.map((year) => yearField(year, "LyngPrlg")), + ], + subtrahendFields: [ + yearField(firstGroupedYear, "DmltnPrlg"), + ...visibleYears.map((year) => yearField(year, "DmltnPrlg")), + ], + type: "differenceOfSums" as const, + }, }, { name: "lyngPrlg", fieldName: "lyngPrlg", header: { text: "매설연장(m)" }, + styleName: "rg-cell-total", footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, @@ -123,19 +148,25 @@ export const PipeTypeStatusInpColumn = { name: "dmltnPrlg", fieldName: "dmltnPrlg", header: { text: "철거연장(m)" }, + styleName: "rg-cell-total", footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", + computed: { + fields: [ + yearField(firstGroupedYear, "DmltnPrlg"), + ...visibleYears.map((year) => yearField(year, "DmltnPrlg")), + ], + type: "sum" as const, + }, }, - ...yyYearCols(1, {}), - ...Array.from({ length: YY_API_MAX - 1 }, (_, i) => { - const yy = i + 2; - return yyYearCols(yy, { + ...yearCols(firstGroupedYear, { footer: footerDecimal2 }), + ...visibleYears.map((year) => + yearCols(year, { footer: footerDecimal2, - visible: yy <= maxYyIndex, - }); - }).flat(), + }), + ).flat(), { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, { name: "admdstCd", @@ -173,7 +204,11 @@ export const PipeTypeStatusInpColumn = { name: "year1996Before", hideChildHeaders: false, header: { text: "1996년 이전" }, - items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"], + items: [ + yearField(firstGroupedYear, "PrlgWhol"), + yearField(firstGroupedYear, "LyngPrlg"), + yearField(firstGroupedYear, "DmltnPrlg"), + ], }, ...yearLayoutItems, "rmrk", diff --git a/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts index 4ef03f741..eca03bda2 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn.ts @@ -9,19 +9,103 @@ export const RegionalFactStatusInpColumn = { { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, - + // 기본 정보 - { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정비기본계획목표년도' }}, - { name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정비기본계획목표년도' } }, + { + name: 'wholPplMtncPlanPrlg', + fieldName: 'wholPplMtncPlanPrlg', + header: { text: '총관로정비계획연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['cbsysPplMtncPlanPrlg', 'drtwPplMtncPlanPrlg', 'exlnPplMtncPlanPrlg'], type: 'sum' as const }, + }, + { + name: 'wholPlanPrlg', + fieldName: 'wholPlanPrlg', + header: { text: '총계획연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { + groups: [ + { + baseField: 'cbsysPplMtncPlanPrlg', + sumFields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg'], + }, + { + baseField: 'drtwPplMtncPlanPrlg', + sumFields: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'], + }, + { + baseField: 'exlnPplMtncPlanPrlg', + sumFields: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'], + }, + ], + type: 'sumOfMaxOfFieldAndSumGroups' as const, + }, + }, + { + name: 'wholFcltPrlg', + fieldName: 'wholFcltPrlg', + header: { text: '총시설연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg', 'drtwPplSqrePrlg', 'drtwPplCiclPrlg', 'exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'], type: 'sum' as const }, + }, + { + name: 'swagePplPsplyRt', + fieldName: 'swagePplPsplyRt', + header: { text: '하수관로보급률(%)' }, + footer: footerDecimal1, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorFields: ['wholFcltPrlg'], + denominatorFields: ['wholPlanPrlg'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 합류식(m) { name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, { name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'cbsysPlanPrlg', + fieldName: 'cbsysPlanPrlg', + header: { text: '계획연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { + baseField: 'cbsysPplMtncPlanPrlg', + sumFields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg'], + type: 'maxOfFieldAndSum' as const, + }, + }, + { + name: 'cbsysFcltPrlg', + fieldName: 'cbsysFcltPrlg', + header: { text: '시설연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg'], type: 'sum' as const }, + }, { name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, @@ -30,16 +114,64 @@ export const RegionalFactStatusInpColumn = { // 분류식(m) > 오수관로 { name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, { name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'drtwPplPlanPrlg', + fieldName: 'drtwPplPlanPrlg', + header: { text: '계획연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { + baseField: 'drtwPplMtncPlanPrlg', + sumFields: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'], + type: 'maxOfFieldAndSum' as const, + }, + }, + { + name: 'drtwPplFcltPrlg', + fieldName: 'drtwPplFcltPrlg', + header: { text: '시설연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'], type: 'sum' as const }, + }, { name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, // 분류식(m) > 우수관로 { name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, { name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, - { name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { + name: 'exlnPplPlanPrlg', + fieldName: 'exlnPplPlanPrlg', + header: { text: '계획연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { + baseField: 'exlnPplMtncPlanPrlg', + sumFields: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'], + type: 'maxOfFieldAndSum' as const, + }, + }, + { + name: 'exlnPplFcltPrlg', + fieldName: 'exlnPplFcltPrlg', + header: { text: '시설연장(m)' }, + footer: footerDecimal2, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + computed: { fields: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'], type: 'sum' as const }, + }, { name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, { name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, @@ -51,7 +183,7 @@ export const RegionalFactStatusInpColumn = { // 분류식 > 오수맨홀, 우수맨홀 { name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - + { name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, @@ -59,7 +191,21 @@ export const RegionalFactStatusInpColumn = { // 하수도대장 전산화 { name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'sewerLdgrEdpRt', + fieldName: 'sewerLdgrEdpRt', + header: { text: '하수도대장전산화율(%)' }, + footer: footerDecimal1, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorFields: ['sewerLdgrEdpPrlg'], + denominatorFields: ['cbsysPplMtncPlanPrlg', 'drtwPplMtncPlanPrlg', 'exlnPplMtncPlanPrlg'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 비고 { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, diff --git a/apps/st/src/components/util/grid/computedFieldCallbacks.ts b/apps/st/src/components/util/grid/computedFieldCallbacks.ts index a19b9f7ea..e9fbeee41 100644 --- a/apps/st/src/components/util/grid/computedFieldCallbacks.ts +++ b/apps/st/src/components/util/grid/computedFieldCallbacks.ts @@ -37,6 +37,90 @@ export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCall }; } +/** 차이값: fields[0] − fields[1] (예: 매설연장 − 철거연장) */ +export function createDifferenceValueCallback(fields: string[]): DataFieldValueCallback { + const a = fields[0] ?? ""; + const b = fields[1] ?? ""; + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + if (!a || !b) return 0; + const ia = fieldNamesArr.indexOf(a); + const ib = fieldNamesArr.indexOf(b); + const va = ia >= 0 ? toNum(values[ia]) : 0; + const vb = ib >= 0 ? toNum(values[ib]) : 0; + return va - vb; + }; +} + +/** (여러 필드 합) - (여러 필드 합) */ +export function createDifferenceOfSumsValueCallback( + minuendFields: string[], + subtrahendFields: string[], +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const sumOf = (fields: string[]) => { + let sum = 0; + for (const f of fields) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) sum += toNum(values[idx]); + } + return sum; + }; + return sumOf(minuendFields) - sumOf(subtrahendFields); + }; +} + +/** 최대값용 valueCallback 생성 */ +export function createMaxValueCallback(fieldNames: string[]): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + let max = Number.NEGATIVE_INFINITY; + for (const f of fieldNames) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) { + const val = toNum(values[idx]); + max = Math.max(max, val); + } + } + return max === Number.NEGATIVE_INFINITY ? 0 : max; + }; +} + +/** 단일 필드값 vs 여러 필드 합계 중 최대값 */ +export function createMaxOfFieldAndSumValueCallback( + baseField: string, + sumFields: string[], +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const baseIdx = fieldNamesArr.indexOf(baseField); + const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0; + let sumVal = 0; + for (const f of sumFields) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) sumVal += toNum(values[idx]); + } + return Math.max(baseVal, sumVal); + }; +} + +/** 여러 그룹의 max(baseField, sum(sumFields)) 값을 모두 합산 */ +export function createSumOfMaxOfFieldAndSumGroupsValueCallback( + groups: Array<{ baseField: string; sumFields: string[] }>, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + let total = 0; + for (const group of groups) { + const baseIdx = fieldNamesArr.indexOf(group.baseField); + const baseVal = baseIdx >= 0 ? toNum(values[baseIdx]) : 0; + let sumVal = 0; + for (const f of group.sumFields) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) sumVal += toNum(values[idx]); + } + total += Math.max(baseVal, sumVal); + } + return total; + }; +} + // 공공하수처리구역인구보급률(%), 고도처리인구보급률(%) valueCallback export function createCoverageRateValueCallback( numeratorFields: string[], @@ -90,6 +174,24 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createSumValueCallback(config.fields); case 'avg': return createAvgValueCallback(config.fields); + case 'max': + return createMaxValueCallback(config.fields); + case 'difference': + return createDifferenceValueCallback(config.fields); + case 'differenceOfSums': + return createDifferenceOfSumsValueCallback( + config.minuendFields, + config.subtrahendFields, + ); + case 'maxOfFieldAndSum': + return createMaxOfFieldAndSumValueCallback( + config.baseField, + config.sumFields, + ); + case 'sumOfMaxOfFieldAndSumGroups': + return createSumOfMaxOfFieldAndSumGroupsValueCallback( + config.groups, + ); case 'pblswrgPopltnSuprtRate': case 'adtrtPopltnSuprtRate': return createCoverageRateValueCallback(config.numeratorFields, config.denominatorFields); diff --git a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index 77572d361..daefd245a 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -21,6 +21,12 @@ import type { StatisticsInputSewPipeService, SeweragePipeInputFactInstallStatusSaveRequest, SeweragePipeInputFactInstallStatusSaveResponse, + SeweragePipeInputPipeTypeStatusSaveRequest, + SeweragePipeInputPipeTypeStatusSaveResponse, + SeweragePipeInputPipeDiameterStatusSaveRequest, + SeweragePipeInputPipeDiameterStatusSaveResponse, + SeweragePipeInputManholeStatusSaveRequest, + SeweragePipeInputManholeStatusSaveResponse, } from "../../model/input/statistics-sewage-pipe-input-interface"; export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeService { @@ -119,6 +125,30 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS ); return response.data; } + + public async savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/save`, + param, + ); + return response.data; + } + + public async savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/save`, + param, + ); + return response.data; + } + + public async saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/save`, + param, + ); + return response.data; + } }; export const statisticsInputSewPipeApiService = new StatisticsInputSewPipeApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts index 80aebe0c6..d592f9684 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts @@ -1,17 +1,4 @@ -import type { ApiResponse } from "../statistics-interface"; - -export interface SewagePipeMaintainRepairStatusInfo { - laePicNm: string | null; - laePicTelNo: string | null; - laeKbrdrDeptNm: string | null; - laeKbrdrJbgdNm: string | null; - laeIdfrNm: string | null; - laeIdfrTelNo: string | null; - laeIdfrDeptNm: string | null; - laeIdfrJbgdNm: string | null; - laeSbmsnSttsCd: string | null; - laeSbmsnYmd: string | null; -} +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; export interface SewagePipeMaintainRepairVerificationResultItem { vrfcCd: string; @@ -57,7 +44,7 @@ export interface SewagePipeMaintainRepairItem { } export interface SewagePipeMaintainRepairData { - statusInfo: SewagePipeMaintainRepairStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SewagePipeMaintainRepairCommonInfo; repairStatusList: SewagePipeMaintainRepairItem[]; } @@ -90,7 +77,7 @@ export interface SewagePipeMaintainDredgingItem { } export interface SewagePipeMaintainDredgingData { - statusInfo: SewagePipeMaintainRepairStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SewagePipeMaintainRepairCommonInfo; pipeDredgingStatusList: SewagePipeMaintainDredgingItem[]; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index 24264cc73..9a3db3ea1 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -1,3 +1,5 @@ +import type { SewerageStatusInfo } from "../statistics-interface"; + export interface SeweragePipeRequestParams { ctpvNm?: string; sggNm?: string; @@ -6,19 +8,6 @@ export interface SeweragePipeRequestParams { mode?: string; } -export interface SeweragePipeInputStatusInfo { - laePicNm: string | null; - laePicTelNo: string | null; - laeKbrdrDeptNm: string | null; - laeKbrdrJbgdNm: string | null; - laeIdfrNm: string | null; - laeIdfrTelNo: string | null; - laeIdfrDeptNm: string | null; - laeIdfrJbgdNm: string | null; - laeSbmsnSttsCd: string | null; - laeSbmsnYmd: string | null; -} - export interface VerifyResultItem { vrfcCd: string; rsnExpln: string; @@ -93,7 +82,7 @@ export interface SeweragePipeInputRegionalFactStatusItem { } export interface SeweragePipeInputRegionalResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; regionalFactStatusList: SeweragePipeInputRegionalFactStatusItem[]; } @@ -133,7 +122,7 @@ export interface SeweragePipeInputRegionalFactStatusSaveResponse { status: string; } -/** 지역별 시설현황 제출 API 요청 (save item 구조 재사용) */ +/** ST00020001 지역별 시설현황 제출 API 요청 (`submitList` 행은 저장과 동일) */ export interface SeweragePipeInputRegionalFactStatusSubmitRequest { statsYr: string; ctpvNm: string; @@ -211,7 +200,7 @@ export interface SeweragePipeInputFactInstallStatusItem { /** 시설별 설치 현황 — data 본문 */ export interface SeweragePipeInputFactInstallResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; factInstallStatusList: SeweragePipeInputFactInstallStatusItem[]; } @@ -223,6 +212,14 @@ export interface SeweragePipeInputFactInstallResponse { message?: string; } +/** 관종별 현황 - 연도별 통계 항목(신규 스펙) */ +export interface SeweragePipeInputPipeTypeYearlyStatItem { + lyngYr: string; + prlgWhol: number; + lyngPrlg: number; + dmltnPrlg: number; +} + /** 관종별 현황 — 목록 항목 */ export interface SeweragePipeInputPipeTypeStatusItem { admdstCd: string; @@ -231,105 +228,20 @@ export interface SeweragePipeInputPipeTypeStatusItem { pipeSeCd: string; pipeSeNm: string; pipeByPrstWhol: number; - lyngPrlg: number; - dmltnPrlg: number; - yy1PrlgWhol: number; - yy1LyngPrlg: number; - yy1DmltnPrlg: number; - yy2PrlgWhol: number; - yy2LyngPrlg: number; - yy2DmltnPrlg: number; - yy3PrlgWhol: number; - yy3LyngPrlg: number; - yy3DmltnPrlg: number; - yy4PrlgWhol: number; - yy4LyngPrlg: number; - yy4DmltnPrlg: number; - yy5PrlgWhol: number; - yy5LyngPrlg: number; - yy5DmltnPrlg: number; - yy6PrlgWhol: number; - yy6LyngPrlg: number; - yy6DmltnPrlg: number; - yy7PrlgWhol: number; - yy7LyngPrlg: number; - yy7DmltnPrlg: number; - yy8PrlgWhol: number; - yy8LyngPrlg: number; - yy8DmltnPrlg: number; - yy9PrlgWhol: number; - yy9LyngPrlg: number; - yy9DmltnPrlg: number; - yy10PrlgWhol: number; - yy10LyngPrlg: number; - yy10DmltnPrlg: number; - yy11PrlgWhol: number; - yy11LyngPrlg: number; - yy11DmltnPrlg: number; - yy12PrlgWhol: number; - yy12LyngPrlg: number; - yy12DmltnPrlg: number; - yy13PrlgWhol: number; - yy13LyngPrlg: number; - yy13DmltnPrlg: number; - yy14PrlgWhol: number; - yy14LyngPrlg: number; - yy14DmltnPrlg: number; - yy15PrlgWhol: number; - yy15LyngPrlg: number; - yy15DmltnPrlg: number; - yy16PrlgWhol: number; - yy16LyngPrlg: number; - yy16DmltnPrlg: number; - yy17PrlgWhol: number; - yy17LyngPrlg: number; - yy17DmltnPrlg: number; - yy18PrlgWhol: number; - yy18LyngPrlg: number; - yy18DmltnPrlg: number; - yy19PrlgWhol: number; - yy19LyngPrlg: number; - yy19DmltnPrlg: number; - yy20PrlgWhol: number; - yy20LyngPrlg: number; - yy20DmltnPrlg: number; - yy21PrlgWhol: number; - yy21LyngPrlg: number; - yy21DmltnPrlg: number; - yy22PrlgWhol: number; - yy22LyngPrlg: number; - yy22DmltnPrlg: number; - yy23PrlgWhol: number; - yy23LyngPrlg: number; - yy23DmltnPrlg: number; - yy24PrlgWhol: number; - yy24LyngPrlg: number; - yy24DmltnPrlg: number; - yy25PrlgWhol: number; - yy25LyngPrlg: number; - yy25DmltnPrlg: number; - yy26PrlgWhol: number; - yy26LyngPrlg: number; - yy26DmltnPrlg: number; - yy27PrlgWhol: number; - yy27LyngPrlg: number; - yy27DmltnPrlg: number; - yy28PrlgWhol: number; - yy28LyngPrlg: number; - yy28DmltnPrlg: number; - yy29PrlgWhol: number; - yy29LyngPrlg: number; - yy29DmltnPrlg: number; - yy30PrlgWhol: number; - yy30LyngPrlg: number; - yy30DmltnPrlg: number; + /** 구 스펙 */ + lyngPrlg?: number; + dmltnPrlg?: number; + /** 신 스펙 */ + lyngPrlgWhol?: number; + dmltnPrlgWhol?: number; + yearlyStatList?: SeweragePipeInputPipeTypeYearlyStatItem[]; rmrk: string; verificationResult?: SeweragePipeInputVerificationResult; } /** 관종별 현황 — data 본문 */ export interface SeweragePipeInputPipeTypeResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; pipeTypeStatusList: SeweragePipeInputPipeTypeStatusItem[]; } @@ -341,6 +253,36 @@ export interface SeweragePipeInputPipeTypeResponse { message?: string; } +/** 관종별 현황 저장 — saveList 항목 */ +export interface SeweragePipeInputPipeTypeStatusSaveItem { + admdstCd: string; + admdstNm: string; + pipeSeCd: string; + yearlyStatList: SeweragePipeInputPipeTypeYearlyStatItem[]; + rmrk: string; + lyngPrlgWhol: number; + dmltnPrlgWhol: number; + pipeByPrstWhol: number; +} + +/** 관종별 현황 저장 API 요청 */ +export interface SeweragePipeInputPipeTypeStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin: string; + saveList: SeweragePipeInputPipeTypeStatusSaveItem[]; +} + +/** 관종별 현황 저장 API 응답 */ +export interface SeweragePipeInputPipeTypeStatusSaveResponse { + data?: string; + message?: string; + status: string; +} + /** 관경별 현황 — 목록 항목 */ export interface SeweragePipeInputPipeDiameterStatusItem { admdstCd: string; @@ -362,7 +304,7 @@ export interface SeweragePipeInputPipeDiameterStatusItem { /** 관경별 현황 — data 본문 */ export interface SeweragePipeInputPipeDiameterResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; pipeDiameterStatusList: SeweragePipeInputPipeDiameterStatusItem[]; } @@ -374,11 +316,69 @@ export interface SeweragePipeInputPipeDiameterResponse { message?: string; } -/** 맨홀 현황 — 목록 항목 (yy14=2014년 … yy25=2025년) */ -export interface SeweragePipeInputManholeStatusItem { +/** 관경별 현황 저장 — saveList 항목 */ +export interface SeweragePipeInputPipeDiameterStatusSaveItem { + pipeSeCd: string; + step1PdmtPrlg: number; + step2PdmtPrlg: number; + step3PdmtPrlg: number; + step4PdmtPrlg: number; + step5PdmtPrlg: number; + sqrePdmtPrlg: number; + etcPdmtPrlg: number; + rmrk: string; + pdmtByTsmWhol: number; + admdstCd: string; + admdstNm: string; +} + +/** 관경별 현황 저장 API 요청 */ +export interface SeweragePipeInputPipeDiameterStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin: string; + saveList: SeweragePipeInputPipeDiameterStatusSaveItem[]; +} + +/** 관경별 현황 저장 API 응답 */ +export interface SeweragePipeInputPipeDiameterStatusSaveResponse { + data?: string; + message?: string; + status: string; +} + +/** 맨홀 현황 — 연도별 통계 항목 */ +export interface SeweragePipeInputManholeYearlyStatItem { + manhYr: string; + wholMnhlCnt: number; + step1MnhlCnt: number; + step2MnhlCnt: number; + step3MnhlCnt: number; + step4MnhlCnt: number; + step5MnhlCnt: number; +} + +type SeweragePipeInputManholeLegacyYy = 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25; +type SeweragePipeInputManholeLegacySuffix = + | "WholMnhlCnt" + | "Step1MnhlCnt" + | "Step2MnhlCnt" + | "Step3MnhlCnt" + | "Step4MnhlCnt" + | "Step5MnhlCnt"; +type SeweragePipeInputManholeLegacyYearField = + `yy${SeweragePipeInputManholeLegacyYy}${SeweragePipeInputManholeLegacySuffix}`; + +/** 맨홀 현황 — 목록 항목 */ +export interface SeweragePipeInputManholeStatusItem + extends Partial> { sggNm: string; admdstCd: string; admdstNm: string; + emphsMngSeCd: string; emphsMngSeNm: string; totalWholMnhlCnt: number; totalStep1MnhlCnt: number; @@ -386,85 +386,14 @@ export interface SeweragePipeInputManholeStatusItem { totalStep3MnhlCnt: number; totalStep4MnhlCnt: number; totalStep5MnhlCnt: number; - yy14WholMnhlCnt: number; - yy14Step1MnhlCnt: number; - yy14Step2MnhlCnt: number; - yy14Step3MnhlCnt: number; - yy14Step4MnhlCnt: number; - yy14Step5MnhlCnt: number; - yy15WholMnhlCnt: number; - yy15Step1MnhlCnt: number; - yy15Step2MnhlCnt: number; - yy15Step3MnhlCnt: number; - yy15Step4MnhlCnt: number; - yy15Step5MnhlCnt: number; - yy16WholMnhlCnt: number; - yy16Step1MnhlCnt: number; - yy16Step2MnhlCnt: number; - yy16Step3MnhlCnt: number; - yy16Step4MnhlCnt: number; - yy16Step5MnhlCnt: number; - yy17WholMnhlCnt: number; - yy17Step1MnhlCnt: number; - yy17Step2MnhlCnt: number; - yy17Step3MnhlCnt: number; - yy17Step4MnhlCnt: number; - yy17Step5MnhlCnt: number; - yy18WholMnhlCnt: number; - yy18Step1MnhlCnt: number; - yy18Step2MnhlCnt: number; - yy18Step3MnhlCnt: number; - yy18Step4MnhlCnt: number; - yy18Step5MnhlCnt: number; - yy19WholMnhlCnt: number; - yy19Step1MnhlCnt: number; - yy19Step2MnhlCnt: number; - yy19Step3MnhlCnt: number; - yy19Step4MnhlCnt: number; - yy19Step5MnhlCnt: number; - yy20WholMnhlCnt: number; - yy20Step1MnhlCnt: number; - yy20Step2MnhlCnt: number; - yy20Step3MnhlCnt: number; - yy20Step4MnhlCnt: number; - yy20Step5MnhlCnt: number; - yy21WholMnhlCnt: number; - yy21Step1MnhlCnt: number; - yy21Step2MnhlCnt: number; - yy21Step3MnhlCnt: number; - yy21Step4MnhlCnt: number; - yy21Step5MnhlCnt: number; - yy22WholMnhlCnt: number; - yy22Step1MnhlCnt: number; - yy22Step2MnhlCnt: number; - yy22Step3MnhlCnt: number; - yy22Step4MnhlCnt: number; - yy22Step5MnhlCnt: number; - yy23WholMnhlCnt: number; - yy23Step1MnhlCnt: number; - yy23Step2MnhlCnt: number; - yy23Step3MnhlCnt: number; - yy23Step4MnhlCnt: number; - yy23Step5MnhlCnt: number; - yy24WholMnhlCnt: number; - yy24Step1MnhlCnt: number; - yy24Step2MnhlCnt: number; - yy24Step3MnhlCnt: number; - yy24Step4MnhlCnt: number; - yy24Step5MnhlCnt: number; - yy25WholMnhlCnt: number; - yy25Step1MnhlCnt: number; - yy25Step2MnhlCnt: number; - yy25Step3MnhlCnt: number; - yy25Step4MnhlCnt: number; - yy25Step5MnhlCnt: number; + yearlyStats?: SeweragePipeInputManholeYearlyStatItem[]; rmrk: string; verificationResult?: SeweragePipeInputVerificationResult; } /** 맨홀 현황 — data 본문 */ export interface SeweragePipeInputManholeResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; manholeStatusList: SeweragePipeInputManholeStatusItem[]; } @@ -476,6 +405,44 @@ export interface SeweragePipeInputManholeResponse { message?: string; } +/** 맨홀 현황 저장 — yearlyStats 항목 */ +export interface SeweragePipeInputManholeStatusSaveYearlyStatItem { + manhYr: string; + wholMnhlCnt: number; + step1MnhlCnt: number; + step2MnhlCnt: number; + step3MnhlCnt: number; + step4MnhlCnt: number; + step5MnhlCnt: number; +} + +/** 맨홀 현황 저장 — saveList 항목 */ +export interface SeweragePipeInputManholeStatusSaveItem { + emphsMngSeCd: string; + admdstCd: string; + admdstNm: string; + yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[]; + rmrk: string; +} + +/** 맨홀 현황 저장 API 요청 */ +export interface SeweragePipeInputManholeStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin: string; + saveList: SeweragePipeInputManholeStatusSaveItem[]; +} + +/** 맨홀 현황 저장 API 응답 */ +export interface SeweragePipeInputManholeStatusSaveResponse { + data?: string; + message?: string; + status: string; +} + /** 시설별 현황 관리 - 하수처리장별 연결 현황 목록 항목 */ export interface SeweragePipeInputFacilityConnectionItem { fcltNm: string; @@ -497,7 +464,7 @@ export interface SeweragePipeInputAdminAreaConnectionItem { /** 시설별 현황 관리 — data 본문 */ export interface SeweragePipeInputConnectionStatusResponseData { - statusInfo: SeweragePipeInputStatusInfo; + statusInfo: SewerageStatusInfo; commonInfo: SeweragePipeInputCommonInfo; facilityConnectionList: SeweragePipeInputFacilityConnectionItem[]; adminAreaConnectionList: SeweragePipeInputAdminAreaConnectionItem[]; @@ -604,4 +571,7 @@ export interface StatisticsInputSewPipeService { saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise; submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise; saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise; + savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise; + savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise; + saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts index de49aee29..319f59e4f 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts @@ -1,23 +1,6 @@ -import type { ApiResponse } from "../statistics-interface"; +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; // 하수도 보급률 데이터 항목 -export interface SewerageSupplyRateStatusInfo { - laeSbmsnSttsCd: string | null; - laeSbmsnYmd: string | null; - laePicNm: string | null; - laePicTelNo: string | null; - laeKbrdrJbgdNm: string | null; - prvnStatus: string | null; - dcsnStatus: string | null; - wrterOpinionSm: string | null; - chckerOpinionSm: string | null; - laeIdfrDeptNm: string | null; - laeIdfrJbgdNm: string | null; - laeIdfrNm: string | null; - laeIdfrTelNo: string | null; - laeKbrdrDeptNm: string | null; -} - export interface SewerageSupplyRateCommonInfo { sewerBzmnArea: number; sewerBzmnCd: string; @@ -84,7 +67,7 @@ export interface SewerageSupplyRateItem { } export interface SewerageSupplyRateResponseData { - statusInfo: SewerageSupplyRateStatusInfo; + statusInfo: SewerageStatusInfo; sewerageSupplyRateList: SewerageSupplyRateItem[]; commonInfo: SewerageSupplyRateCommonInfo; } diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 00383e994..841db1ff3 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -35,6 +35,21 @@ export interface ApiResponse { message?: string; } +/** 하수 입력/보급률 등 조회 `data.statusInfo` 공통 (LAE 제출·담당자 정보) */ +export interface SewerageStatusInfo { + laePicNm: string | null; + laePicTelNo: string | null; + laeKbrdrDeptNm: string | null; + laeKbrdrJbgdNm: string | null; + laeIdfrNm: string | null; + laeIdfrTelNo: string | null; + laeIdfrDeptNm: string | null; + laeIdfrJbgdNm: string | null; + laeSbmsnSttsCd: string | null; + laeSbmsnYmd: string | null; + rjctOpnnCn: string | null; +} + export interface ImrRequest { user_id?: string; sewer_bzmn_cd: string; @@ -268,17 +283,17 @@ export interface ReviewSubmitResponse { message?: string; } +export interface SubmitVerifyExceptionItem { + vrfcSn: number; + prcsOpnnExpln: string; +} + export interface SubmitVerifyExceptionRequest { sewerBzmnCd: string; statsYr: string; statsCd: string; userId: string; - exceptionList: [ - { - vrfcSn: number; - prcsOpnnExpln: string; - } - ]; + exceptionList: SubmitVerifyExceptionItem[]; } export interface SubmitVerifyExceptionResponse { From 0bb2b4d052997e76efa6cab36eb100060afda653 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 7 May 2026 16:30:12 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[st]=20chore:=20=EC=95=8C=EB=A6=BC=ED=86=A1?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/NoticeTalkSending.tsx | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/st/src/components/statistics/management/NoticeTalkSending.tsx b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx index cdb060e30..ff5031193 100644 --- a/apps/st/src/components/statistics/management/NoticeTalkSending.tsx +++ b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx @@ -77,6 +77,16 @@ interface SelectedRecipient extends SewerStUserItem { variableOverrides: Record; } +type CommonVariableKey = + | "userNm" + | "sido" + | "gugun" + | "gubunNm" + | "deptNm" + | "jbgdNm" + | "telNo" + | "custom"; + const EMPTY_ADST_LIST: AdstZoneResponseDto[] = []; const GUBUN_OPTIONS = [ { value: "PIC", label: "입력자" }, @@ -139,18 +149,7 @@ function NtitlkSndng() { Record >({}); const [commonVariableMapping, setCommonVariableMapping] = useState< - Record< - string, - | "userNm" - | "sido" - | "gugun" - | "gubunNm" - | "deptNm" - | "jbgdNm" - | "telNo" - | "custom" - | "" - > + Record >({}); const [previewRecipient, setPreviewRecipient] = @@ -286,7 +285,7 @@ function NtitlkSndng() { rowSelection: userListRowSelection, }, onRowSelectionChange: setUserListRowSelection, - getRowId: (row) => `${row.userNm}-${row.telNo}`, + getRowId: (row, i) => `${i}-${row.telNo}`, }); const selectedTemplate = useMemo( @@ -527,7 +526,7 @@ function NtitlkSndng() { -
+

미리보기

@@ -640,7 +639,7 @@ function NtitlkSndng() {
-
+
{isUserListLoading ? (
@@ -659,7 +658,7 @@ function NtitlkSndng() { )}
{!isUserListLoading && userList.length > 0 && ( -
+

선택{" "} @@ -692,6 +691,14 @@ function NtitlkSndng() { {selectedRecipients.length}명 +