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/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}명 +