Files
frontend/apps/st/src/components/statistics/input/SewagePipeInp.tsx
T

2379 lines
119 KiB
TypeScript

import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import {
STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues,
} from "@/shared/lib/stats-cd-input-navigation";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SeweragePipeInputFactInstallStatusSaveRequest,
SeweragePipeInputFactInstallStatusSubmitRequest,
SeweragePipeInputManholeStatusItem,
SeweragePipeInputManholeStatusSaveRequest,
SeweragePipeInputManholeStatusSaveYearlyStatItem,
SeweragePipeInputManholeStatusSubmitRequest,
SeweragePipeInputPipeDiameterStatusSaveRequest,
SeweragePipeInputPipeDiameterStatusSubmitRequest,
SeweragePipeInputPipeTypeStatusItem,
SeweragePipeInputPipeTypeStatusSaveRequest,
SeweragePipeInputPipeTypeStatusSubmitRequest,
SeweragePipeInputRegionalFactStatusSaveItem,
SeweragePipeInputRegionalFactStatusSaveRequest,
SeweragePipeInputRegionalFactStatusSubmitRequest,
SeweragePipeInputSubmitResponse,
SeweragePipeInputVerificationResult,
SeweragePipeRequestParams,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
import {
type CheckInputPeriodItem,
getVerificationPeriodActionBlockedMessage,
isInputPeriodCurrent,
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
cancelSubmitRequest,
ImrStatusResponse,
SewerageStatusInfo,
SubmitExceptionRequest,
Sido,
} from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid";
import GridTopActionButton from "@/components/module/GridTopActionButton";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import SplitDataGrid from "@/components/module/SplitDataGrid";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList, ColumnLayoutItem } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
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 { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
/** 탭별 status + 입력기간으로 편집 가능 여부 */
const computeTabEnterable = (
statusInfo: SewerageStatusInfo | null | undefined,
periodEnterable: boolean,
): boolean => {
if (statusInfo?.laeSbmsnSttsCd) {
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
return false;
}
return periodEnterable;
}
return periodEnterable;
};
function buildSewagePipeSearchParam(
values: SearchFormValues,
overrides?: { mode?: string },
): SeweragePipeRequestParams {
return {
statsYr: values.year,
ctpvNm: values.sido,
...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}),
};
}
const getPipeTypeLastDataYear = () => {
const lastDataYear = new Date().getFullYear() - 1;
return Math.max(1995, lastDataYear);
};
const PIPE_TYPE_STATUS_TAB_KEY = "ST00020003";
/** 조회 연도(statsYr)에 해당하는 연도 (y{year}* 필드용) */
const resolvePipeTypeEditableYear = (statsYr: string | null | undefined): number | null => {
if (!statsYr) return null;
const year = Number(statsYr);
if (!Number.isFinite(year)) return null;
return Math.min(getPipeTypeLastDataYear(), Math.max(1995, year));
};
/** 조회 연도(statsYr)에 해당하는 연도별 매설연장 필드명 (y{year}LyngPrlg) */
const resolvePipeTypeEditableLyngField = (statsYr: string | null | undefined): string | null => {
const year = resolvePipeTypeEditableYear(statsYr);
return year === null ? null : `y${year}LyngPrlg`;
};
const pipeTypeYearDmltnField = (year: number) => `y${year}DmltnPrlg`;
const shouldHidePipeTypeEditableYearDmltn = (
canEdit: boolean,
periodEnterable: boolean,
statsYr: string | null,
): number | null => {
if (!canEdit || !periodEnterable) return null;
return resolvePipeTypeEditableYear(statsYr);
};
const applyPipeTypeStatusColumnLayout = (
layout: ColumnLayoutItem[] | undefined,
hideDmltnYear: number | null,
): ColumnLayoutItem[] => {
if (hideDmltnYear === null) return layout ?? [];
const hideField = pipeTypeYearDmltnField(hideDmltnYear);
const walk = (items: ColumnLayoutItem[]): ColumnLayoutItem[] => {
const result: ColumnLayoutItem[] = [];
for (const item of items) {
if (typeof item === "string") {
if (item !== hideField) result.push(item);
continue;
}
if ("column" in item) {
if (item.column !== hideField) result.push(item);
continue;
}
const nextItems = walk(item.items as ColumnLayoutItem[]);
if (nextItems.length > 0) {
result.push({ ...item, items: nextItems });
}
}
return result;
};
return walk(layout ?? []);
};
/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능, 해당 연도 철거연장 컬럼 숨김 */
const applyPipeTypeStatusEditableColumns = (
cols: ColumnConfig[] | undefined,
canEdit: boolean,
periodEnterable: boolean,
statsYr: string | null,
): ColumnConfig[] => {
const hideDmltnYear = shouldHidePipeTypeEditableYearDmltn(canEdit, periodEnterable, statsYr);
return (cols ?? []).map((col) => {
if (!canEdit) {
return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" };
}
const fieldName = col.fieldName ?? col.name;
if (hideDmltnYear !== null && fieldName === pipeTypeYearDmltnField(hideDmltnYear)) {
return {
...col,
visible: false,
editable: false as const,
excludeFromExcelExport: true,
};
}
const isYearLyngField =
fieldName !== "lyngPrlg" && typeof fieldName === "string" && fieldName.endsWith("LyngPrlg");
if (fieldName === "lyngPrlg" || isYearLyngField) {
const editableLyngField = periodEnterable ? resolvePipeTypeEditableLyngField(statsYr) : null;
const isTargetYear = editableLyngField !== null && fieldName === editableLyngField;
if (isTargetYear) {
const { styleName: _readonlyStyle, ...rest } = col;
return {
...rest,
editable: true as const,
};
}
return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" };
}
return col;
});
};
const getManholeLastDataYear = () => {
const lastDataYear = new Date().getFullYear() - 1;
return Math.max(2014, lastDataYear);
};
/** 2-3 관종별: verificationResult.prlgWhol → 총계 계(pipeByPrstWhol) */
function remapPipeTypePrlgWholVerification(
result?: SeweragePipeInputVerificationResult | null,
): SeweragePipeInputVerificationResult | null | undefined {
if (!result) return result;
const fromAlias = result.prlgWhol;
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
const existing = result.pipeByPrstWhol;
if (Array.isArray(existing) && existing.length > 0) return result;
return { ...result, pipeByPrstWhol: fromAlias };
}
/** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */
function normalizePipeTypeStatusRow(
row: SeweragePipeInputPipeTypeStatusItem,
ctpvNm: string,
): Record<string, string | number | SeweragePipeInputVerificationResult | undefined> {
const lastDataYear = getPipeTypeLastDataYear();
const {
yearlyStatList,
lyngPrlgWhol,
dmltnPrlgWhol,
verificationResult,
...rest
} = row;
const yearFields: Record<string, number> = {};
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,
verificationResult: remapPipeTypePrlgWholVerification(verificationResult) ?? undefined,
};
}
/** 맨홀 신 응답(yearlyStats) -> 그리드 yy{14..25}* 필드로 정규화 */
function normalizeManholeStatusRow(
row: SeweragePipeInputManholeStatusItem,
ctpvNm: string,
): Record<string, string | number | SeweragePipeInputVerificationResult | undefined> {
const maxYy = getManholeLastDataYear() % 100;
const { yearlyStats, ...rest } = row;
const yearFields: Record<string, number> = {};
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<string, string | number> & {
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<void>((resolve) => setTimeout(resolve, ms));
async function waitForImrJobCompleted(
jobId: string,
getStatus: (id: string) => Promise<ImrStatusResponse>,
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<void> {
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 || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
}
await sleep(pollIntervalMs);
}
throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
}
function buildRegionalFactSubmitListFromGrid(
currentData: Record<string, string | number>[],
): SeweragePipeInputRegionalFactStatusSaveItem[] {
const columnFormatMap = new Map<string, string>();
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: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(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: nullableTrimmedString(d.rmrk),
}));
}
function tabRowDataExists(
m: Map<string, Record<string, string | number>[]>,
tabKey: string,
): boolean {
if (tabKey === "ST00020006") {
const left = m.get("treatmentPlantConnectionInp");
const right = m.get("adminAreaConnectionInp");
return (
(Array.isArray(left) && left.length > 0) ||
(Array.isArray(right) && right.length > 0)
);
}
const rows = m.get(tabKey);
return Array.isArray(rows) && rows.length > 0;
}
/** 조회 직후 해당 탭에만 저장하는 상단 툴바 노출 조건 */
type TabToolbarSnapshot = {
showSaveInToolbar: boolean;
showEnterableToolbarButtons: boolean;
showActionToolbarCard: boolean;
};
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return isGugunOkForToolbar(sidoListRef.current, values);
}
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
/**
* @param searchContext 조회에 사용된 검색값(해당 요청 기준으로 구군·사업자 조건 반영)
* @param treatAsSearched (hasRow || 이 값) 일 때 카드 후보 — load 내부에서는 searchDoneRef 반영용
*/
function computeTabToolbarSnapshot(
statusInfo: SewerageStatusInfo | null,
hasRowData: boolean,
treatAsSearched: boolean,
searchContext: SearchFormValues | null,
periodEnterable: boolean,
isAdminInfo: boolean,
): TabToolbarSnapshot {
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
(hasRowData || treatAsSearched) &&
(showSaveInToolbar ||
showEnterableToolbarButtons ||
showSubmittedInInputPeriodToolbar ||
(isSubmitted && isAdminInfo));
return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard };
}
// 하수도 통계 관리 > 02. 하수관로
const SewagePipeInp = () => {
const { sidoList, isReady } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const { setMessage: alert } = useAlertStore((state) => state);
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
const [showDailog, setShowDailog] = useState<boolean>(false);
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
const [hasSearched, setHasSearched] = useState(false);
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
/** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(
() => new Map(),
);
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
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 toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getFactInstallGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getPipeTypeGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getPipeDiameterGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getManholeGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const searchParamRef = useRef<SeweragePipeRequestParams | null>(null);
/** 검색 시 checkInputPeriod 결과 — 탭 전환 지연 조회 시 편집 가능 여부와 동일하게 맞춤 */
const periodEnterableRef = useRef(true);
/** 검색 완료 후(API 연결 전) 탭별 지연 로그용 — 첫 탭에 행이 없어도 동작하도록 보조 */
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const postVerificationSearchRef = useRef<{
values: SearchFormValues;
tabKey: string;
} | null>(null);
/** ST00020002 무데이터 안내/자동이동은 동일 조회조건에서 1회만 수행 */
const factInstallNoDataHandledKeyRef = useRef<string | null>(null);
const [verificationSearchDefaults, setVerificationSearchDefaults] =
useState<SearchFormValues | null>(null);
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001";
const activeTabRef = useRef<string>(firstTabKey);
useEffect(() => {
activeTabRef.current = activeTab;
}, [activeTab]);
const activeTabId = useTabStore((s) => s.activeTabId);
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
useLayoutEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
if (!isReady) return;
const pending = useStatisticsInputFocusStore
.getState()
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE);
if (!pending) return;
const { tabKey, search } = pending;
activeTabRef.current = tabKey;
setActiveTab(tabKey);
if (search) {
const values = verificationSearchToFormValues(search, sidoList);
setVerificationSearchDefaults(values);
postVerificationSearchRef.current = { values, tabKey };
setSearchFormRemountKey((k) => k + 1);
}
}, [activeTabId, inputFocusNonce, isReady, sidoList]);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
let cancelled = false;
void (async () => {
const next = await fetchColumnGuidesRecord(tabKey);
if (cancelled) return;
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, [activeTabId, activeTab, columnGuidesByTab]);
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
if (loadedTabKey === activeTabRef.current) {
setStatsCd(nextStatsCd);
}
};
const handleReset = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setLastSearchedYear(null);
setLastSearchedValues(null);
setHasSearched(false);
setInputPeriodEnterable(true);
setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
periodEnterableRef.current = true;
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
setStatsCd("");
setPeriodCheckList([]);
setVerificationSearchDefaults(null);
setSearchFormRemountKey((k) => k + 1);
};
const columnInfo = useMemo(() => {
const applyColumnGuides = (cols: ColumnConfig[] | undefined, tabKey: string) =>
(cols ?? []).map((col: ColumnConfig) => {
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
return {
...col,
tooltip: guide && guide.length > 0 ? guide : undefined,
};
});
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
const canEdit =
(computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const applyEditable = (cols: ColumnConfig[] | undefined) =>
tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY
? applyPipeTypeStatusEditableColumns(
cols,
canEdit,
inputPeriodEnterable,
lastSearchedYear,
)
: (cols ?? []).map((col: ColumnConfig) =>
canEdit
? col
: {
...col,
editable: false as const,
styleName: col.styleName ?? "rg-cell-readonly",
},
);
if (tab.left && tab.right) {
return {
...tab,
left: { ...tab.left, columns: applyColumnGuides(applyEditable(tab.left.columns), tab.tabKey) },
right: { ...tab.right, columns: applyColumnGuides(applyEditable(tab.right.columns), tab.tabKey) },
};
}
return {
...tab,
columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey),
...(tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY && tab.columnLayout
? {
columnLayout: applyPipeTypeStatusColumnLayout(
tab.columnLayout,
shouldHidePipeTypeEditableYearDmltn(
canEdit,
inputPeriodEnterable,
lastSearchedYear,
),
),
}
: {}),
};
});
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab, lastSearchedYear]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SewerageStatusInfo | null,
hasRowData: boolean,
searchContext: SearchFormValues,
) => {
setToolbarSnapshotByTab((prev) => {
const next = new Map(prev);
next.set(
tabKey,
computeTabToolbarSnapshot(
statusInfo,
hasRowData,
searchDoneRef.current,
searchContext,
periodEnterableRef.current,
isAdminInfo,
),
);
return next;
});
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
};
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
const loadSewagePipeActiveTabData = async (
tabKey: string,
searchParam: SeweragePipeRequestParams,
values: SearchFormValues,
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
): Promise<boolean> => {
const searchConditionKey = JSON.stringify({
statsYr: searchParam.statsYr ?? "",
ctpvNm: searchParam.ctpvNm ?? "",
sggNm: values?.gugun === "ALL" ? "" : (values?.gugun ?? searchParam.sggNm ?? ""),
sewerBzmnCd: searchParam.sewerBzmnCd ?? "",
});
const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => {
if (mergeIntoExistingMap) {
setRowDataMap((prev) => {
const next = new Map(prev);
apply(next);
return next;
});
} else {
const next = new Map<string, Record<string, string | number>[]>();
apply(next);
setRowDataMap(next);
}
};
const filterFetched = <T,>(list: readonly T[] | null | undefined) =>
filterBySelectedAdmdstCd(list, sidoListRef.current, values);
if (tabKey === 'ST00020001') {
const response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const convertedData = filterFetched(response.data.regionalFactStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm,
...d,
}));
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (tabKey === 'ST00020002') {
const response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const convertedData = filterFetched(response.data.factInstallStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm,
...d,
}));
if (convertedData.length === 0) {
const alreadyHandledForSameSearch =
factInstallNoDataHandledKeyRef.current === searchConditionKey;
if (!alreadyHandledForSameSearch) {
factInstallNoDataHandledKeyRef.current = searchConditionKey;
alert("시설별 설치 현황에 조회된 데이터가 없습니다.\n시설별 현황 관리에서 설정해주세요");
activeTabRef.current = "ST00020006";
setActiveTab("ST00020006");
return await loadSewagePipeActiveTabData(
"ST00020006",
searchParam,
values,
true,
updateLastSearchedValues,
);
}
}
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (tabKey === 'ST00020003') {
const response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const convertedData = filterFetched(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));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, {
verificationResult:
remapPipeTypePrlgWholVerification(commonInfo.verificationResult) ??
null,
}),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (tabKey === 'ST00020004') {
const response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const convertedData = filterFetched(response.data.pipeDiameterStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm,
...d,
}));
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (tabKey === 'ST00020005') {
const response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const convertedData = filterFetched(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));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
convertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (tabKey === 'ST00020006') {
const response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam);
if (response.status === 'success') {
const commonInfo = response.data.commonInfo;
const facilityConvertedData = filterFetched(response.data.facilityConnectionList).map((d) => ({
...d,
}));
const adminConvertedData = filterFetched(response.data.adminAreaConnectionList).map((d) => ({
...d,
ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
}));
applyRowMap((m) => {
m.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
m.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
tabKey,
response.data.statusInfo,
facilityConvertedData.length > 0 || adminConvertedData.length > 0,
values,
);
return true;
}
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
return false;
}
if (!mergeIntoExistingMap) {
setRowDataMap(new Map());
}
if (updateLastSearchedValues) setLastSearchedValues(values);
return true;
};
const handleSearch = async (
values: SearchFormValues,
options?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
try {
const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
if (periodResponse.status !== 'success') {
alert(periodResponse.message);
return;
}
const periods = Array.isArray(periodResponse.data) ? periodResponse.data : [];
setPeriodCheckList(periods);
const periodOk = isInputPeriodCurrent(periods);
periodEnterableRef.current = periodOk;
setInputPeriodEnterable(periodOk);
/** 연도가 바뀐 검색만 전 탭 캐시 초기화 + 첫 탭으로 이동. 연도 외 조건만 바뀌면 다른 탭은 이전 조회 데이터 유지 */
const yearChangedFromLastSearch =
lastSearchedValues != null &&
(lastSearchedValues.year ?? "") !== (values.year ?? "");
if (yearChangedFromLastSearch) {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setStatsCd("");
setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
}
lazyTabLoggedRef.current.clear();
setIsLoading(true);
const searchParam = buildSewagePipeSearchParam(values);
searchParamRef.current = searchParam;
searchDoneRef.current = true;
setHasSearched(true);
setLastSearchedYear(values.year ?? null);
const forcedTab = options?.forcedTabKey;
const tabKeyToLoad = forcedTab
? forcedTab
: yearChangedFromLastSearch
? firstTabKey
: activeTab;
if (forcedTab) {
activeTabRef.current = forcedTab;
setActiveTab(forcedTab);
} else if (yearChangedFromLastSearch) {
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
} else {
activeTabRef.current = activeTab;
}
await loadSewagePipeActiveTabData(tabKeyToLoad, searchParam, values, true, true);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const job = postVerificationSearchRef.current;
if (!job) return;
postVerificationSearchRef.current = null;
void handleSearch(job.values, { forcedTabKey: job.tabKey });
}, [searchFormRemountKey]);
const getCurrentRowsForTab = (tabKey: string): Record<string, string | number>[] => {
switch (tabKey) {
case REGIONAL_FACT_TAB_KEY:
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 rowDataMap.get(tabKey) ?? [];
}
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getCurrentRowsForTab(prevTabKey);
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(prevTabKey, latestRowsInPrevTab);
return next;
});
}
activeTabRef.current = tabKey;
setActiveTab(tabKey);
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey);
const canLazyLoad =
searchParamRef.current &&
!hasActiveTabData &&
!lazyTabLoggedRef.current.has(tabKey) &&
searchDoneRef.current;
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
const values = lastSearchedValues ?? searchValues;
if (!searchParam || !values) return;
try {
setIsLoading(true);
const ok = await loadSewagePipeActiveTabData(tabKey, searchParam, values, true, false);
if (ok) lazyTabLoggedRef.current.add(tabKey);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
const getLastYearDataFunc = async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
}
if (!lastSearchedYear) {
alert('먼저 데이터를 조회해주세요.');
return;
}
try {
setIsLoading(true);
const param = buildSewagePipeSearchParam(values, { mode: 'PRE' });
await loadSewagePipeActiveTabData(activeTab, param, values, true, false);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
// const getTabGridRowsForVerification = (tabKey: string): Record<string, string | number>[] => {
// 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;
}
const userInfo = await getUserInfo();
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? statsCd;
const params: SubmitExceptionRequest = {
...submitInfo,
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? "",
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;
const submitBase = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
};
let response: SeweragePipeInputSubmitResponse | null = 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: SeweragePipeInputRegionalFactStatusSubmitRequest = {
...submitBase,
submitList,
};
setGlobalProgressLabel("일반 검증 진행 중...");
response =
await statisticsInputSewPipeApiService.submitRegionalFactStatus(requestData);
break;
}
case "ST00020002": {
const currentData = getFactInstallGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
FactInstallStatusInpColumn.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 submitList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"),
mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"),
cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"),
sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"),
sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"),
sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"),
icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"),
atlfcsPrlg: formatNumber(d.atlfcsPrlg, "atlfcsPrlg"),
cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"),
sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"),
sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"),
wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"),
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
}));
const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = {
...submitBase,
submitList,
};
setGlobalProgressLabel("일반 검증 진행 중...");
response = await statisticsInputSewPipeApiService.submitFactInstallStatus(requestData);
break;
}
case "ST00020003": {
const currentData = getPipeTypeGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
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 submitList = 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: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
pipeSeCd: nullableTrimmedString(d.pipeSeCd),
yearlyStatList,
rmrk: nullableTrimmedString(d.rmrk),
lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"),
dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"),
pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"),
};
});
const requestData: SeweragePipeInputPipeTypeStatusSubmitRequest = {
...submitBase,
submitList,
};
setGlobalProgressLabel("일반 검증 진행 중...");
response = await statisticsInputSewPipeApiService.submitPipeTypeStatus(requestData);
break;
}
case "ST00020004": {
const currentData = getPipeDiameterGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
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 submitList = currentData.map((d) => ({
pipeSeCd: nullableTrimmedString(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: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
}));
const requestData: SeweragePipeInputPipeDiameterStatusSubmitRequest = {
...submitBase,
submitList,
};
setGlobalProgressLabel("일반 검증 진행 중...");
response = await statisticsInputSewPipeApiService.submitPipeDiameterStatus(requestData);
break;
}
case "ST00020005": {
const currentData = getManholeGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
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 submitList = currentData.map((d) => {
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = [];
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: nullableTrimmedString(d.emphsMngSeCd),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
yearlyStats,
rmrk: nullableTrimmedString(d.rmrk),
};
});
const requestData: SeweragePipeInputManholeStatusSubmitRequest = {
...submitBase,
submitList,
};
setGlobalProgressLabel("일반 검증 진행 중...");
response = await statisticsInputSewPipeApiService.submitManholeStatus(requestData);
break;
}
default:
alert("해당 탭 제출 API는 준비 중입니다.");
return;
}
if (!response || response.status !== "success") {
alert(response?.message || "데이터 제출 중 오류가 발생했습니다.");
return;
}
const finalStatus = response.data?.finalStatus;
const nvpassed = response.data?.nvpassed;
if (finalStatus === "5") {
setGlobalProgress(50);
setGlobalProgressLabel("시계열 검증 진행 중...");
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,
);
await openVerificationResultDialogIfNeeded(
{
...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "",
statsYr: values.year ?? "",
statsCd: statsCdForTab,
},
setVerificationResultDialogOpen,
{ notify: alert },
);
} catch (e) {
await loadSewagePipeActiveTabData(
tabKey,
buildSewagePipeSearchParam(values),
values,
true,
false,
);
await openVerificationResultDialogIfNeeded(
{
...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "",
statsYr: values.year ?? "",
statsCd: statsCdForTab,
},
setVerificationResultDialogOpen,
{ notify: alert, notifySubmitComplete: 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,
);
await openVerificationResultDialogIfNeeded(
{
...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "",
statsYr: values.year ?? "",
statsCd: statsCdForTab,
},
setVerificationResultDialogOpen,
{ notify: alert },
);
}
} catch {
alert("데이터 제출 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
};
const cnacelSubmitFunc = async () => {
try {
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '',
statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
};
const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === 'success') {
const values = lastSearchedValues ?? searchValues;
if (values) {
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
}
alert('제출 취소되었습니다.');
} else {
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
}
}
} catch {
alert('제출 취소 중 오류가 발생했습니다.');
}
};
const refreshConnectionStatusAfterPopupSave = async () => {
const searchParam = searchParamRef.current;
const values = lastSearchedValues ?? searchValues;
if (!searchParam || !values) return;
await loadSewagePipeActiveTabData("ST00020006", searchParam, values, true, false);
};
const openVerificationHistoryDialog = () => {
const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? statsCd;
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !activeTabStatsCd) {
alert("먼저 조회를 진행해주세요.");
return;
}
setVerificationHistoryOpen(true);
};
const saveFunc = async () => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
alert(verificationBlocked);
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
const values = lastSearchedValues ?? searchValues;
if (!values?.year) {
alert("검색 후 저장해 주세요.");
return;
}
try {
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
switch (activeTab) {
case "ST00020001": {
const currentData = getGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
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);
};
const userInfo = await getUserInfo();
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: nullableTrimmedString(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: nullableTrimmedString(d.rmrk),
}));
const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
const response =
await statisticsInputSewPipeApiService.saveRegionalFactStatus(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 "ST00020002": {
const currentData = getFactInstallGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
FactInstallStatusInpColumn.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 userInfo = await getUserInfo();
const saveList = currentData.map((d) => ({
admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: nullableTrimmedString(d.sggNm),
admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"),
mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"),
cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"),
sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"),
sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"),
sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"),
icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"),
atlfcsPrlg: formatNumber(d.atlfcsPrlg, "atlfcsPrlg"),
cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"),
sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"),
sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"),
wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"),
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
rmrk: nullableTrimmedString(d.rmrk),
}));
const param: SeweragePipeInputFactInstallStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList,
};
const response =
await statisticsInputSewPipeApiService.saveFactInstallStatus(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 "ST00020003": {
const currentData = getPipeTypeGridDataRef.current?.();
if (!currentData || currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
const columnFormatMap = new Map<string, string>();
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: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
pipeSeCd: nullableTrimmedString(d.pipeSeCd),
yearlyStatList,
rmrk: nullableTrimmedString(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 ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "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<string, string>();
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: nullableTrimmedString(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: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
}));
const userInfo = await getUserInfo();
const param: SeweragePipeInputPipeDiameterStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "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<string, string>();
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: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = [];
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: nullableTrimmedString(d.emphsMngSeCd),
admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
yearlyStats,
rmrk: nullableTrimmedString(d.rmrk),
};
});
const userInfo = await getUserInfo();
const param: SeweragePipeInputManholeStatusSaveRequest = {
statsYr: values.year ?? "",
ctpvNm: values.sido ?? "",
...toInputSearchRegion(sidoListRef.current, values),
userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "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;
}
} catch {
alert("데이터 저장 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
};
const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const gugunOkOrNotDoList = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar =
computeTabToolbarSnapshot(
activeTabStatus,
tabRowDataExists(rowDataMap, activeTab),
hasSearched,
searchContextForToolbar,
inputPeriodEnterable,
isAdminInfo,
);
const hideToolbarForConnectionTab = activeTab === "ST00020006";
const {
showSaveInToolbar: baseShowSaveInToolbar,
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
showActionToolbarCard: baseShowActionToolbarCard,
} = activeTabToolbar;
const showSaveInToolbar =
!hideToolbarForConnectionTab && baseShowSaveInToolbar && toolbarGroupAllowed;
const showEnterableToolbarButtons =
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard =
!hideToolbarForConnectionTab && baseShowActionToolbarCard && toolbarGroupAllowed;
return (
<>
<SearchForm
isInput
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
/>
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
{activeTab !== 'ST00020002' && <LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />}
{submitOk && <SubmitButton submitFunc={() => void submitFunc(false)} />}
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
)}
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
const tabEnterableForGrid = computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
);
const excelToolbarEnabledForTab =
tabRowDataExists(rowDataMap, tabKey) &&
tabEnterableForGrid &&
gugunOkOrNotDoList;
if (info.isSplitGrid && info.left && info.right) {
const leftKey = info.left.tabKey || `${tabKey}_left`;
const rightKey = info.right.tabKey || `${tabKey}_right`;
const leftRowData = rowDataMap.get(leftKey) ?? [];
const rightRowData = rowDataMap.get(rightKey) ?? [];
const splitTopButton = info.right.topButton;
const showSplitTopButton =
activeTab === tabKey &&
Boolean(splitTopButton?.text) &&
(splitTopButton?.visible ?? true) &&
tabRowDataExists(rowDataMap, tabKey) &&
gugunOkOrNotDoList;
const leftColumnInfo: ColumnInfo = {
columns: info.left.columns ?? [],
columnLayout: info.left.columnLayout ?? [],
};
const rightColumnInfo: ColumnInfo = {
columns: info.right.columns ?? [],
columnLayout: info.right.columnLayout ?? [],
};
return (
<TabsContent key={tabKey} value={tabKey}>
<div className="flex flex-col gap-[0.6rem]">
<div className="flex justify-end gap-[0.6rem]">
{showSplitTopButton && (
<GridTopActionButton
text={splitTopButton?.text ?? ""}
onClick={splitTopButton?.onClick}
statsYr={lastSearchedYear ?? undefined}
initialSearchValues={
lastSearchedValues
? {
sido: lastSearchedValues.sido,
gugun: lastSearchedValues.gugun,
bsnmCd: lastSearchedValues.bsnmCd,
}
: null
}
onSaveSuccess={refreshConnectionStatusAfterPopupSave}
/>
)}
</div>
<SplitDataGrid
leftColumnInfo={leftColumnInfo}
rightColumnInfo={rightColumnInfo}
leftFixedColumns={info.left.fixedColumns}
rightFixedColumns={info.right.fixedColumns}
showHeaderCheckbox={false}
enableSort={false}
leftRowData={leftRowData}
rightRowData={rightRowData}
setLeftRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(leftKey) ?? [];
const n =
typeof updater === "function"
? updater(cur)
: updater;
next.set(leftKey, n);
return next;
});
}}
setRightRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(rightKey) ?? [];
const n =
typeof updater === "function"
? updater(cur)
: updater;
next.set(rightKey, n);
return next;
});
}}
/>
</div>
</TabsContent>
);
}
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={tabKey}
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={excelToolbarEnabledForTab}
excelImportSkipNonEditable={
tabKey === PIPE_TYPE_STATUS_TAB_KEY
? !isAdminInfo
: false
}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
statusInfo={tabStatusInfoMap.get(tabKey) ?? null}
getGridDataRef={
tabKey === REGIONAL_FACT_TAB_KEY
? getGridDataRef
: tabKey === "ST00020002"
? getFactInstallGridDataRef
: tabKey === "ST00020003"
? getPipeTypeGridDataRef
: tabKey === "ST00020004"
? getPipeDiameterGridDataRef
: tabKey === "ST00020005"
? getManholeGridDataRef
: undefined
}
cellStyleCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
}}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return errors
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln}
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
.filter(Boolean)
.join("\n");
}
}}
/>
</TabsContent>
);
})}
</Tabs>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={
searchValues?.year &&
searchValues?.sido &&
searchValues?.bsnmCd &&
(tabStatsCdMap.get(activeTab) ?? statsCd)
? {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido,
statsYr: searchValues.year,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
}
: null
}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
}}
/>
<SubmitDailog
open={showDailog}
onOpenChange={(open) => {
setShowDailog(open);
if (!open) setIsExceptionSubmitFlow(false);
}}
statusInfo={tabStatusInfoMap.get(activeTab) ?? null}
onConfirm={executeSubmit}
onExceptionConfirm={executeExceptionSubmit}
submitMode={isExceptionSubmitFlow ? "exception" : "normal"}
isLoading={isLoading}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={
searchValues?.year &&
searchValues?.sido &&
searchValues?.bsnmCd &&
(tabStatsCdMap.get(activeTab) ?? statsCd)
? {
...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido,
statsYr: searchValues.year,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
}
: null
}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
}}
/>
</>
);
};
export default SewagePipeInp;