Files
frontend/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx
T
2026-08-20 11:48:18 +09:00

1270 lines
52 KiB
TypeScript

import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import {
STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues,
} from "@/shared/lib/stats-cd-input-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewagePipeMaintainDredgingSaveItem,
SewagePipeMaintainDredgingSaveRequest,
SewagePipeMaintainDredgingSubmitRequest,
SewagePipeMaintainRepairSaveItem,
SewagePipeMaintainRepairSaveRequest,
SewagePipeMaintainRepairSubmitRequest,
SewagePipeMaintainRepairVerificationResult,
SewagePipeMaintainRequestParams,
} from "@/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface";
import { 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,
} from "@/entities/statistics/model/statistics-interface";
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 { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
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 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";
function buildSewagePipeMaintainSearchParam(
values: SearchFormValues,
overrides?: { mode?: string },
): SewagePipeMaintainRequestParams {
return {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
...(overrides?.mode ? { mode: overrides.mode } : {}),
};
}
const SewagePipeMaintainInpTabList = [
RepairStatusInpColumn,
PipeDredgingStatusInpColumn,
] as const;
const firstTabKey = RepairStatusInpColumn.tabKey;
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
columns: tab.columns ?? [],
columnLayout: tab.columnLayout ?? [],
});
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewagePipeMaintainRepairVerificationResult;
};
type TabToolbarSnapshot = {
showSaveInToolbar: boolean;
showEnterableToolbarButtons: boolean;
showActionToolbarCard: boolean;
};
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 buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "")
? Boolean(values?.gugun && values.gugun !== "ALL")
: true;
}
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 };
}
function tabRowDataExists(
m: Map<string, Record<string, string | number>[]>,
tabKey: string,
): boolean {
const rows = m.get(tabKey);
return Array.isArray(rows) && rows.length > 0;
}
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
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 buildColumnFormatNumber(
columns: { fieldName?: string; dataFormat?: string }[],
) {
const columnFormatMap = new Map<string, string>();
columns.forEach((col) => {
if (col.fieldName && col.dataFormat) {
columnFormatMap.set(col.fieldName, col.dataFormat);
}
});
return (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);
};
}
function buildRepairRowList(
currentData: Record<string, string | number>[],
): SewagePipeMaintainRepairSaveItem[] {
const formatNumber = buildColumnFormatNumber(RepairStatusInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
mendngWhol: formatNumber(d.mendngWhol, "mendngWhol"),
cbsysMendngPrlg: formatNumber(d.cbsysMendngPrlg, "cbsysMendngPrlg"),
sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"),
sprsysDrtwPrlg: formatNumber(d.sprsysDrtwPrlg, "sprsysDrtwPrlg"),
cbsysPartMendngPrlg: formatNumber(d.cbsysPartMendngPrlg, "cbsysPartMendngPrlg"),
sprsysExlnMendngPrlg: formatNumber(d.sprsysExlnMendngPrlg, "sprsysExlnMendngPrlg"),
sprsysDrtwMendngPrlg: formatNumber(d.sprsysDrtwMendngPrlg, "sprsysDrtwMendngPrlg"),
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"),
otflCnt: formatNumber(d.otflCnt, "otflCnt"),
cctvExmnPrlg: formatNumber(d.cctvExmnPrlg, "cctvExmnPrlg"),
rmrk: normalizeGridRmrk(d.rmrk),
}));
}
function buildDredgingRowList(
currentData: Record<string, string | number>[],
): SewagePipeMaintainDredgingSaveItem[] {
const formatNumber = buildColumnFormatNumber(PipeDredgingStatusInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"),
sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"),
sprsysDrtwPrlg: formatNumber(d.sprsysDrtwPrlg, "sprsysDrtwPrlg"),
mnhlCnt: formatNumber(d.mnhlCnt, "mnhlCnt"),
exlnDrtwRcvprpCnt: formatNumber(d.exlnDrtwRcvprpCnt, "exlnDrtwRcvprpCnt"),
stofcbrOtflCnt: formatNumber(d.stofcbrOtflCnt, "stofcbrOtflCnt"),
drdgnPlanPrlg: formatNumber(d.drdgnPlanPrlg, "drdgnPlanPrlg"),
drdgnTrgtCtrsDrdgnRt: formatNumber(d.drdgnTrgtCtrsDrdgnRt, "drdgnTrgtCtrsDrdgnRt"),
wholPrlgCtrsDrdgnRt: formatNumber(d.wholPrlgCtrsDrdgnRt, "wholPrlgCtrsDrdgnRt"),
drdgnAmt: formatNumber(d.drdgnAmt, "drdgnAmt"),
drdgnQnt: formatNumber(d.drdgnQnt, "drdgnQnt"),
drdgnUntprcAmt: formatNumber(d.drdgnUntprcAmt, "drdgnUntprcAmt"),
wholDrdgnPrcsQnt: formatNumber(d.wholDrdgnPrcsQnt, "wholDrdgnPrcsQnt"),
rclmQnt: formatNumber(d.rclmQnt, "rclmQnt"),
oceanEmsnQnt: formatNumber(d.oceanEmsnQnt, "oceanEmsnQnt"),
rcycQnt: formatNumber(d.rcycQnt, "rcycQnt"),
etcQnt: formatNumber(d.etcQnt, "etcQnt"),
rmrk: normalizeGridRmrk(d.rmrk),
}));
}
// 하수도 통계 관리 > 03. 하수관로 유지관리
const SewagePipeMaintainInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { isAdminInfo } = useAdminStore();
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
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 [isLoading, setIsLoading] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
Map<string, SewerageStatusInfo | null>
>(new Map());
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 [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
const searchParamRef = useRef<SewagePipeMaintainRequestParams | null>(null);
const postVerificationSearchRef = useRef<{
values: SearchFormValues;
tabKey: string;
} | null>(null);
const [verificationSearchDefaults, setVerificationSearchDefaults] =
useState<SearchFormValues | null>(null);
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
const [rowDataMap, setRowDataMap] = useState<
Map<string, Record<string, string | number>[]>
>(new Map());
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>>
>({});
const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true);
const activeTabRef = useRef(activeTab);
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
useLayoutEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
const pending = useStatisticsInputFocusStore
.getState()
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN);
if (!pending) return;
const { tabKey, search } = pending;
setActiveTab(tabKey);
if (search) {
const values = verificationSearchToFormValues(search);
setVerificationSearchDefaults(values);
postVerificationSearchRef.current = { values, tabKey };
setSearchFormRemountKey((k) => k + 1);
}
}, [activeTabId, inputFocusNonce]);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false;
void (async () => {
const next = await fetchColumnGuidesRecord(statsCd);
if (cancelled) return;
setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})();
return () => {
cancelled = true;
};
}, [activeTabId, activeTab, columnGuidesByTab]);
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;
});
};
const applyRowMap = (
mergeIntoExistingMap: boolean,
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 loadActiveTabData = async (
tabKey: string,
param: SewagePipeMaintainRequestParams,
searchContext: SearchFormValues,
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
overrides?: { mode?: string },
): Promise<boolean> => {
const requestParam = overrides?.mode
? { ...param, mode: overrides.mode }
: param;
if (tabKey === RepairStatusInpColumn.tabKey) {
const response =
await sewagePipeMaintainApiService.getSewagePipeMaintainRepair(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, repairStatusList } = response.data;
const convertedData = (repairStatusList ?? []).map((d) => ({
ctpvNm: commonInfo.ctpvNm,
...d,
}));
const gridRows = toGridRows(convertedData);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(RepairStatusInpColumn.tabKey, gridRows);
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
commitToolbarSnapshotForLoadedTab(
tabKey,
statusInfo,
gridRows.length > 0,
searchContext,
);
return true;
}
if (tabKey === PipeDredgingStatusInpColumn.tabKey) {
const response =
await sewagePipeMaintainApiService.getSewagePipeMaintainDredging(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, pipeDredgingStatusList } = response.data;
const convertedData = (pipeDredgingStatusList ?? []).map((d) => ({
ctpvNm: commonInfo.ctpvNm,
...d,
}));
const gridRows = toGridRows(convertedData);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(PipeDredgingStatusInpColumn.tabKey, gridRows);
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
commitToolbarSnapshotForLoadedTab(
tabKey,
statusInfo,
gridRows.length > 0,
searchContext,
);
return true;
}
alert("해당 탭 조회 API가 없습니다.");
return false;
};
const getLastYearDataFunc = async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
if (!hasSearched) {
alert("먼저 데이터를 조회해주세요.");
return;
}
try {
setIsLoading(true);
const param = buildSewagePipeMaintainSearchParam(values, { mode: "PRE" });
await loadActiveTabData(
activeTabRef.current,
param,
values,
true,
false,
{ mode: "PRE" },
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const getTabGridRows = (tabKey: string): Record<string, string | number>[] =>
getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? [];
const getRepairGridRows = (): Record<string, string | number>[] =>
getTabGridRows(RepairStatusInpColumn.tabKey);
const getDredgingGridRows = (): Record<string, string | number>[] =>
getTabGridRows(PipeDredgingStatusInpColumn.tabKey);
const submitFunc = () => {
void (async () => {
const tabKey = activeTabRef.current;
const isRepairTab = tabKey === RepairStatusInpColumn.tabKey;
const isDredgingTab = tabKey === PipeDredgingStatusInpColumn.tabKey;
if (!isRepairTab && !isDredgingTab) {
alert("제출할 수 없는 탭입니다.");
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
const currentData = isRepairTab ? getRepairGridRows() : getDredgingGridRows();
if (!currentData.length) {
alert("제출할 데이터가 없습니다.");
return;
}
if (await confirm("제출 하시겠습니까?")) {
setShowSubmitDialog(true);
}
})();
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const tabKey = activeTabRef.current;
const isRepairTab = tabKey === RepairStatusInpColumn.tabKey;
const isDredgingTab = tabKey === PipeDredgingStatusInpColumn.tabKey;
if (!isRepairTab && !isDredgingTab) {
alert("제출할 수 없는 탭입니다.");
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert("먼저 조회를 진행해주세요.");
return;
}
const currentData = isRepairTab ? getRepairGridRows() : getDredgingGridRows();
if (!currentData.length) {
alert("제출할 데이터가 없습니다.");
return;
}
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
};
const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab,
};
const reloadSubmittedTab = async () => {
await loadActiveTabData(
tabKey,
searchParamRef.current!,
lastSearchedValues,
true,
false,
);
};
try {
showGlobalProgress({ label: "서버에 요청 중...", progress: 0 });
const userInfo = await getUserInfo();
setGlobalProgressLabel("일반 검증 진행 중...");
const response = isRepairTab
? await sewagePipeMaintainApiService.submitSewagePipeMaintainRepair({
...submitInfoBase,
userId: userInfo.userUnqId,
submitList: buildRepairRowList(currentData),
} satisfies SewagePipeMaintainRepairSubmitRequest)
: await sewagePipeMaintainApiService.submitSewagePipeMaintainDredging({
...submitInfoBase,
userId: userInfo.userUnqId,
submitList: buildDredgingRowList(currentData),
} satisfies SewagePipeMaintainDredgingSubmitRequest);
if (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: lastSearchedValues.bsnmCd ?? "",
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.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 reloadSubmittedTab();
await openVerificationResultDialogIfNeeded(
verificationQuery,
setVerificationResultDialogOpen,
{ notify: alert },
);
} catch (e) {
await reloadSubmittedTab();
await openVerificationResultDialogIfNeeded(verificationQuery, setVerificationResultDialogOpen, {
notify: alert,
notifySubmitComplete: false,
});
const msg =
e instanceof Error && e.message
? e.message
: "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.";
alert(msg);
}
} else {
setGlobalProgress(100);
await reloadSubmittedTab();
await openVerificationResultDialogIfNeeded(
verificationQuery,
setVerificationResultDialogOpen,
{ notify: alert },
);
}
} catch {
alert("데이터 제출 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
};
const saveFunc = () => {
void (async () => {
const tabKey = activeTabRef.current;
const isRepairTab = tabKey === RepairStatusInpColumn.tabKey;
const isDredgingTab = tabKey === PipeDredgingStatusInpColumn.tabKey;
if (!isRepairTab && !isDredgingTab) {
alert("저장할 수 없는 탭입니다.");
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
const baseRequest = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
};
try {
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
const userInfo = await getUserInfo();
if (isRepairTab) {
const currentData = getRepairGridRows();
if (!currentData.length) {
alert("저장할 데이터가 없습니다.");
return;
}
const param: SewagePipeMaintainRepairSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildRepairRowList(currentData),
};
if (isAdminInfo) {
param.superAdmin = "superAdmin";
}
const response =
await sewagePipeMaintainApiService.saveSewagePipeMaintainRepair(
param,
);
if (response.status === "success") {
setGlobalProgress(50);
await loadActiveTabData(
RepairStatusInpColumn.tabKey,
searchParamRef.current,
lastSearchedValues,
true,
false,
);
setGlobalProgress(100);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
return;
}
const currentData = getDredgingGridRows();
if (!currentData.length) {
alert("저장할 데이터가 없습니다.");
return;
}
const param: SewagePipeMaintainDredgingSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildDredgingRowList(currentData),
};
if (isAdminInfo) {
param.superAdmin = "superAdmin";
}
const response =
await sewagePipeMaintainApiService.saveSewagePipeMaintainDredging(
param,
);
if (response.status === "success") {
setGlobalProgress(50);
await loadActiveTabData(
PipeDredgingStatusInpColumn.tabKey,
searchParamRef.current,
lastSearchedValues,
true,
false,
);
setGlobalProgress(100);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
} catch {
alert("데이터 저장 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
})();
};
const cnacelSubmitFunc = () => {
void (async () => {
try {
if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return;
if (!lastSearchedValues || !searchParamRef.current) {
alert("먼저 조회를 진행해주세요.");
return;
}
const userInfo = await getUserInfo();
const tabKey = activeTabRef.current;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab,
};
const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === "success") {
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
true,
false,
);
alert("제출 취소되었습니다.");
} else {
alert(response.message ?? "제출 취소 중 오류가 발생했습니다.");
}
} catch {
alert("제출 취소 중 오류가 발생했습니다.");
}
})();
};
const handleReset = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
setLastSearchedValues(null);
setHasSearched(false);
setInputPeriodEnterable(true);
searchParamRef.current = null;
setVerificationSearchDefaults(null);
setSearchFormRemountKey((k) => k + 1);
searchDoneRef.current = false;
periodEnterableRef.current = true;
};
const handleSearch = async (
values: SearchFormValues,
opts?: { 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
: [];
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());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current;
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
const searchParam = buildSewagePipeMaintainSearchParam(values);
searchParamRef.current = searchParam;
const ok = await loadActiveTabData(
tabForLoad,
searchParam,
values,
!yearChangedFromLastSearch,
true,
);
if (!ok) return;
} 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 handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
const latestRowsInPrevTab = getTabGridRows(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);
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
if (tabRowDataExists(rowDataMap, tabKey)) return;
try {
setIsLoading(true);
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
true,
false,
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const columnInfoByTab = useMemo(() => {
const map = new Map<string, ColumnInfo>();
for (const tab of SewagePipeMaintainInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = tab.columns ?? [];
const base = canEdit
? rawColumns
: rawColumns.map((col) => ({ ...col, editable: false as const }));
const columns = base
.map((col) => {
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
return guide ? { ...col, tooltip: guide } : col;
});
map.set(tabKey, {
columns,
columnLayout: tab.columnLayout ?? [],
});
}
return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = lastSearchedValues ?? searchValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot(
activeTabStatus,
tabRowDataExists(rowDataMap, activeTab),
hasSearched,
searchContextForToolbar,
inputPeriodEnterable,
isAdminInfo,
);
const {
showSaveInToolbar: baseShowSaveInToolbar,
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
showActionToolbarCard: baseShowActionToolbarCard,
} = activeTabToolbar;
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
const showEnterableToolbarButtons =
baseShowEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard =
baseShowActionToolbarCard && toolbarGroupAllowed;
const openVerificationHistoryDialog = () => {
const ctx = lastSearchedValues ?? searchValues;
const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) {
alert("먼저 조회를 진행해주세요.");
return;
}
setVerificationHistoryOpen(true);
};
const verificationQueryContext = (() => {
const ctx = lastSearchedValues ?? searchValues;
const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return {
sewerBzmnCd: ctx.bsnmCd,
ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year,
statsCd,
};
})();
const verificationCellStyle = ({
fieldName,
row,
}: {
fieldName: string;
row: Record<string, string | number>;
}): string | undefined => {
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";
}
};
const verificationCellTooltip = ({
fieldName,
row,
}: {
fieldName: string;
row: Record<string, string | number>;
}): string | undefined => {
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" ? "일반검증 - " : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
})
.filter(Boolean)
.join("\n");
}
};
return (
<>
<div className="relative">
<SearchForm
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] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
<LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()}
/>
<SubmitButton submitFunc={submitFunc} />
</>
)}
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<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>
{SewagePipeMaintainInpTabList.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{SewagePipeMaintainInpTabList.map((info) => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
columnInfo={
columnInfoByTab.get(tabKey) ??
toGridColumnInfo(info)
}
excelExportFileSuffix={info.tabLabel || tabKey}
fixedColumns={info.fixedColumns}
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;
});
}}
showHeaderCheckbox={false}
enableSort={false}
statusInfo={tabStatusInfo}
isExcelImport={
tabRowDataExists(rowDataMap, tabKey) &&
tabEnterableForGrid &&
gugunOk
}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values || !searchParamRef.current) return;
await loadActiveTabData(
activeTabRef.current,
searchParamRef.current,
values,
true,
false,
);
}}
/>
<ConfirmDialog isOpen={false} />
<SubmitDailog
open={showSubmitDialog}
onOpenChange={setShowSubmitDialog}
statusInfo={activeTabStatus}
onConfirm={executeSubmit}
isLoading={isLoading}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={verificationQueryContext}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values || !searchParamRef.current) return;
await loadActiveTabData(
activeTabRef.current,
searchParamRef.current,
values,
true,
false,
);
}}
/>
</>
);
};
export default SewagePipeMaintainInp;