1284 lines
54 KiB
TypeScript
1284 lines
54 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
|
|
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/api/input/statistics-input-private-sewage-facility-service";
|
|
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
|
import type {
|
|
PrivateSewageDesigStatusSaveItem,
|
|
PrivateSewageFacilityVerificationResult,
|
|
PrivateSewageDesigStatusItem,
|
|
PrivateSewageDesigStatusSaveRequest,
|
|
PrivateSewageDesigStatusSubmitRequest,
|
|
SepticTankStatusSaveItem,
|
|
SepticTankStatusSaveRequest,
|
|
SepticTankStatusSubmitRequest,
|
|
SewageTreatFactStatusItem,
|
|
SewageTreatFactStatusSaveItem,
|
|
SewageTreatFactStatusSaveRequest,
|
|
SewageTreatFactStatusSubmitRequest,
|
|
SepticTankStatusItem,
|
|
SewageTreatFactStatusRequestParams,
|
|
} from "@/entities/statistics/model/input/statistics-input-private-sewage-facility-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 SubmitButton from "@/components/button/SumitButton";
|
|
import SaveButton from "@/components/button/SaveButton";
|
|
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
|
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import SearchForm from "@/components/module/SearchForm";
|
|
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
|
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
|
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
|
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
|
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
|
import { doList } from "@/components/util/column/input/DoList";
|
|
import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn";
|
|
import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn";
|
|
import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn";
|
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
|
import { useAdminStore } from "@/store/admin-store";
|
|
|
|
const PrivateSewageFacilityInpTabList = [
|
|
SewageTreatFactStatusInpColumn,
|
|
SepticTankStatusInpColumn,
|
|
PrivateSewageDesigStatusInpColumn,
|
|
] as const;
|
|
|
|
const firstTabKey = SewageTreatFactStatusInpColumn.tabKey;
|
|
|
|
/** getColumnGuides API는 statsCd 기준 — 3번째 탭 tabKey와 statsCd가 다름 */
|
|
const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
|
|
[SewageTreatFactStatusInpColumn.tabKey]: SewageTreatFactStatusInpColumn.tabKey,
|
|
[SepticTankStatusInpColumn.tabKey]: SepticTankStatusInpColumn.tabKey,
|
|
[PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003",
|
|
};
|
|
|
|
const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from(
|
|
new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)),
|
|
);
|
|
|
|
type GridRowWithVerification = Record<string, string | number> & {
|
|
verificationResult?: PrivateSewageFacilityVerificationResult;
|
|
};
|
|
|
|
type TabToolbarSnapshot = {
|
|
showSaveInToolbar: boolean;
|
|
showEnterableToolbarButtons: boolean;
|
|
showActionToolbarCard: boolean;
|
|
};
|
|
|
|
const toGridColumnInfo = (tab: (typeof PrivateSewageFacilityInpTabList)[number]): ColumnInfo => ({
|
|
columns: tab.columns ?? [],
|
|
columnLayout: tab.columnLayout ?? [],
|
|
});
|
|
|
|
function buildPrivateSewageFacilitySearchParam(
|
|
values: SearchFormValues,
|
|
overrides?: { mode?: string },
|
|
): SewageTreatFactStatusRequestParams {
|
|
return {
|
|
statsYr: values.year,
|
|
sewerBzmnCd: values.bsnmCd,
|
|
ctpvNm: values.sido,
|
|
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
|
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
|
};
|
|
}
|
|
|
|
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) && 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 mapRegionalListToGridRows<
|
|
T extends { verificationResult?: PrivateSewageFacilityVerificationResult },
|
|
>(list: T[], ctpvNm: string): GridRowWithVerification[] {
|
|
return list.map((item) => {
|
|
const { verificationResult, ...rest } = item;
|
|
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
|
|
if (verificationResult) row.verificationResult = verificationResult;
|
|
return row;
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 buildSewageTreatFactStatusSaveList(
|
|
currentData: Record<string, string | number>[],
|
|
): SewageTreatFactStatusSaveItem[] {
|
|
const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns);
|
|
|
|
return currentData.map((d) => ({
|
|
admdstCd: normalizeGridString(d.admdstCd),
|
|
admdstNm: normalizeGridString(d.admdstNm),
|
|
step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"),
|
|
step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"),
|
|
step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"),
|
|
step4StaaPrcsQnt: formatNumber(d.step4StaaPrcsQnt, "step4StaaPrcsQnt"),
|
|
step5StaaPrcsQnt: formatNumber(d.step5StaaPrcsQnt, "step5StaaPrcsQnt"),
|
|
step6StaaPrcsQnt: formatNumber(d.step6StaaPrcsQnt, "step6StaaPrcsQnt"),
|
|
step7StaaPrcsQnt: formatNumber(d.step7StaaPrcsQnt, "step7StaaPrcsQnt"),
|
|
step1StaaExcpPrcsQnt: formatNumber(d.step1StaaExcpPrcsQnt, "step1StaaExcpPrcsQnt"),
|
|
step2StaaExcpPrcsQnt: formatNumber(d.step2StaaExcpPrcsQnt, "step2StaaExcpPrcsQnt"),
|
|
step3StaaExcpPrcsQnt: formatNumber(d.step3StaaExcpPrcsQnt, "step3StaaExcpPrcsQnt"),
|
|
step4StaaExcpPrcsQnt: formatNumber(d.step4StaaExcpPrcsQnt, "step4StaaExcpPrcsQnt"),
|
|
step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"),
|
|
step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"),
|
|
step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"),
|
|
rmrk: normalizeGridRmrk(d.rmrk),
|
|
}));
|
|
}
|
|
|
|
function buildSepticTankStatusSaveList(
|
|
currentData: Record<string, string | number>[],
|
|
): SepticTankStatusSaveItem[] {
|
|
const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns);
|
|
|
|
return currentData.map((d) => ({
|
|
admdstCd: normalizeGridString(d.admdstCd),
|
|
admdstNm: normalizeGridString(d.admdstNm),
|
|
staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"),
|
|
step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"),
|
|
staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"),
|
|
staaInsdAertMthdCnt: formatNumber(d.staaInsdAertMthdCnt, "staaInsdAertMthdCnt"),
|
|
staaInsdCntcAertCnt: formatNumber(d.staaInsdCntcAertCnt, "staaInsdCntcAertCnt"),
|
|
staaInsdEtcCnt: formatNumber(d.staaInsdEtcCnt, "staaInsdEtcCnt"),
|
|
step1StaaInsdNope: formatNumber(d.step1StaaInsdNope, "step1StaaInsdNope"),
|
|
step2StaaInsdNope: formatNumber(d.step2StaaInsdNope, "step2StaaInsdNope"),
|
|
step3StaaInsdNope: formatNumber(d.step3StaaInsdNope, "step3StaaInsdNope"),
|
|
step4StaaInsdNope: formatNumber(d.step4StaaInsdNope, "step4StaaInsdNope"),
|
|
step5StaaInsdNope: formatNumber(d.step5StaaInsdNope, "step5StaaInsdNope"),
|
|
step6StaaInsdNope: formatNumber(d.step6StaaInsdNope, "step6StaaInsdNope"),
|
|
step7StaaInsdNope: formatNumber(d.step7StaaInsdNope, "step7StaaInsdNope"),
|
|
step8StaaInsdNope: formatNumber(d.step8StaaInsdNope, "step8StaaInsdNope"),
|
|
staaExcpSttkNprsflCnt: formatNumber(d.staaExcpSttkNprsflCnt, "staaExcpSttkNprsflCnt"),
|
|
step1StaaExcpSttkFrcCnt: formatNumber(d.step1StaaExcpSttkFrcCnt, "step1StaaExcpSttkFrcCnt"),
|
|
staaExcpSprktpFltrtbCnt: formatNumber(d.staaExcpSprktpFltrtbCnt, "staaExcpSprktpFltrtbCnt"),
|
|
staaExcpAertMthdCnt: formatNumber(d.staaExcpAertMthdCnt, "staaExcpAertMthdCnt"),
|
|
staaExcpCntcAertCnt: formatNumber(d.staaExcpCntcAertCnt, "staaExcpCntcAertCnt"),
|
|
staaExcpEtcCnt: formatNumber(d.staaExcpEtcCnt, "staaExcpEtcCnt"),
|
|
step1StaaExcpNope: formatNumber(d.step1StaaExcpNope, "step1StaaExcpNope"),
|
|
step2StaaExcpNope: formatNumber(d.step2StaaExcpNope, "step2StaaExcpNope"),
|
|
step3StaaExcpNope: formatNumber(d.step3StaaExcpNope, "step3StaaExcpNope"),
|
|
step4StaaExcpNope: formatNumber(d.step4StaaExcpNope, "step4StaaExcpNope"),
|
|
step5StaaExcpNope: formatNumber(d.step5StaaExcpNope, "step5StaaExcpNope"),
|
|
step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"),
|
|
step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"),
|
|
step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"),
|
|
rmrk: normalizeGridRmrk(d.rmrk),
|
|
}));
|
|
}
|
|
|
|
function buildPrivateSewageDesigStatusSaveList(
|
|
currentData: Record<string, string | number>[],
|
|
): PrivateSewageDesigStatusSaveItem[] {
|
|
const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns);
|
|
|
|
return currentData.map((d) => ({
|
|
admdstCd: normalizeGridString(d.admdstCd),
|
|
admdstNm: normalizeGridString(d.admdstNm),
|
|
indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"),
|
|
indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"),
|
|
spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"),
|
|
spctnkQnt: formatNumber(d.spctnkQnt, "spctnkQnt"),
|
|
drtwPrcsFcltGrssFcltCnt: formatNumber(
|
|
d.drtwPrcsFcltGrssFcltCnt,
|
|
"drtwPrcsFcltGrssFcltCnt",
|
|
),
|
|
drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"),
|
|
rmrk: normalizeGridRmrk(d.rmrk),
|
|
}));
|
|
}
|
|
|
|
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 || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
|
}
|
|
await sleep(pollIntervalMs);
|
|
}
|
|
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
|
}
|
|
|
|
function resolvePrivateSewageFacilityTab(tabKey: string) {
|
|
return {
|
|
isSewageTreatTab: tabKey === SewageTreatFactStatusInpColumn.tabKey,
|
|
isSepticTankTab: tabKey === SepticTankStatusInpColumn.tabKey,
|
|
isDesigStatusTab: tabKey === PrivateSewageDesigStatusInpColumn.tabKey,
|
|
};
|
|
}
|
|
|
|
// 하수도 통계 관리 > 08. 개인하수처리시설
|
|
const PrivateSewageFacilityInp = () => {
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const { confirm } = useConfirm();
|
|
const { isAdminInfo } = useAdminStore();
|
|
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 [isLoading, setIsLoading] = useState(false);
|
|
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
|
|
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
|
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
|
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 [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
|
|
Map<string, TabToolbarSnapshot>
|
|
>(new Map());
|
|
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
|
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
|
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
|
|
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
|
Record<string, Record<string, string>>
|
|
>({});
|
|
|
|
const searchParamRef = useRef<SewageTreatFactStatusRequestParams | null>(null);
|
|
const searchDoneRef = useRef(false);
|
|
const periodEnterableRef = useRef(true);
|
|
const activeTabRef = useRef(activeTab);
|
|
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
|
activeTabRef.current = activeTab;
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void (async () => {
|
|
const entries = await Promise.all(
|
|
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
|
|
try {
|
|
const res = await statisticsApi.getColumnGuides(statsCd);
|
|
if (res.status !== "success" || !Array.isArray(res.data)) {
|
|
return [statsCd, {}] as const;
|
|
}
|
|
const next: Record<string, string> = {};
|
|
for (const item of res.data) {
|
|
const key = item.engColNm?.trim();
|
|
const guide = item.columnGuide?.trim();
|
|
if (key && guide) next[key] = guide;
|
|
}
|
|
return [statsCd, next] as const;
|
|
} catch {
|
|
return [statsCd, {}] as const;
|
|
}
|
|
}),
|
|
);
|
|
if (cancelled) return;
|
|
const guidesByStatsCd = Object.fromEntries(entries);
|
|
setColumnGuidesByTab(
|
|
Object.fromEntries(
|
|
Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
|
|
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
|
|
),
|
|
),
|
|
);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
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: SewageTreatFactStatusRequestParams,
|
|
searchContext: SearchFormValues,
|
|
mergeIntoExistingMap: boolean,
|
|
updateLastSearchedValues: boolean,
|
|
overrides?: { mode?: string },
|
|
): Promise<boolean> => {
|
|
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
|
|
|
|
if (tabKey === SewageTreatFactStatusInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.getSewageTreatFactStatusList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, sewageTreatFactStatusList } = response.data;
|
|
const gridRows = mapRegionalListToGridRows(
|
|
(sewageTreatFactStatusList ?? []) as SewageTreatFactStatusItem[],
|
|
commonInfo.ctpvNm,
|
|
);
|
|
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(SewageTreatFactStatusInpColumn.tabKey, gridRows);
|
|
});
|
|
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
|
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
|
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
|
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
|
return true;
|
|
}
|
|
|
|
if (tabKey === SepticTankStatusInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.getSepticTankStatusList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, septicTankStatusList } = response.data;
|
|
const gridRows = mapRegionalListToGridRows(
|
|
(septicTankStatusList ?? []) as SepticTankStatusItem[],
|
|
commonInfo.ctpvNm,
|
|
);
|
|
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(SepticTankStatusInpColumn.tabKey, gridRows);
|
|
});
|
|
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
|
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
|
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
|
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
|
return true;
|
|
}
|
|
|
|
if (tabKey === PrivateSewageDesigStatusInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.getPrivateSewageDesigStatusList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, privateSewageDesigStatusList } = response.data;
|
|
const gridRows = mapRegionalListToGridRows(
|
|
(privateSewageDesigStatusList ?? []) as PrivateSewageDesigStatusItem[],
|
|
commonInfo.ctpvNm,
|
|
);
|
|
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(PrivateSewageDesigStatusInpColumn.tabKey, gridRows);
|
|
});
|
|
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
|
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
|
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
|
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
|
return true;
|
|
}
|
|
|
|
alert("해당 탭 조회 API가 없습니다.");
|
|
return false;
|
|
};
|
|
|
|
const getTabGridRows = (tabKey: string): Record<string, string | number>[] =>
|
|
getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? [];
|
|
|
|
const handleReset = () => {
|
|
setRowDataMap(new Map());
|
|
setTabStatusInfoMap(new Map());
|
|
setTabStatsCdMap(new Map());
|
|
setToolbarSnapshotByTab(new Map());
|
|
setActiveTab(firstTabKey);
|
|
activeTabRef.current = firstTabKey;
|
|
setLastSearchedValues(null);
|
|
setHasSearched(false);
|
|
setInputPeriodEnterable(true);
|
|
searchParamRef.current = null;
|
|
searchDoneRef.current = false;
|
|
periodEnterableRef.current = true;
|
|
};
|
|
|
|
const handleSearch = async (values: SearchFormValues) => {
|
|
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());
|
|
setToolbarSnapshotByTab(new Map());
|
|
activeTabRef.current = firstTabKey;
|
|
setActiveTab(firstTabKey);
|
|
}
|
|
|
|
setIsLoading(true);
|
|
searchDoneRef.current = true;
|
|
setHasSearched(true);
|
|
const searchParam = buildPrivateSewageFacilitySearchParam(values);
|
|
searchParamRef.current = searchParam;
|
|
|
|
await loadActiveTabData(
|
|
activeTabRef.current,
|
|
searchParam,
|
|
values,
|
|
!yearChangedFromLastSearch,
|
|
true,
|
|
);
|
|
} catch {
|
|
alert("데이터 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
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 lastYearDataFunc = async () => {
|
|
const values = lastSearchedValues ?? searchValues;
|
|
if (!values?.sido || values.sido === "ALL") {
|
|
alert("시도를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!hasSearched) {
|
|
alert("먼저 데이터를 조회해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const param = buildPrivateSewageFacilitySearchParam(values, { mode: "PRE" });
|
|
await loadActiveTabData(
|
|
activeTabRef.current,
|
|
param,
|
|
values,
|
|
true,
|
|
false,
|
|
{ mode: "PRE" },
|
|
);
|
|
} catch {
|
|
alert("데이터 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
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);
|
|
if (!statsCdForTab) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
|
|
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") {
|
|
setIsLoading(true);
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
lastSearchedValues,
|
|
false,
|
|
false,
|
|
);
|
|
alert("제출 취소되었습니다.");
|
|
} else {
|
|
alert(response.message ?? "제출 취소 중 오류가 발생했습니다.");
|
|
}
|
|
} catch {
|
|
alert("제출 취소 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const submitFunc = () => {
|
|
void (async () => {
|
|
const tabKey = activeTabRef.current;
|
|
const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } =
|
|
resolvePrivateSewageFacilityTab(tabKey);
|
|
if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) {
|
|
alert("제출할 수 없는 탭입니다.");
|
|
return;
|
|
}
|
|
if (!lastSearchedValues || !searchParamRef.current) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
const currentData = getTabGridRows(tabKey);
|
|
if (!currentData.length) {
|
|
alert("제출할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
if (await confirm("제출 하시겠습니까?")) {
|
|
setShowSubmitDialog(true);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
|
const tabKey = activeTabRef.current;
|
|
const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } =
|
|
resolvePrivateSewageFacilityTab(tabKey);
|
|
if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) {
|
|
alert("제출할 수 없는 탭입니다.");
|
|
return;
|
|
}
|
|
if (!lastSearchedValues || !searchParamRef.current) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
|
|
const currentData = getTabGridRows(tabKey);
|
|
if (!currentData.length) {
|
|
alert("제출할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
|
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
|
const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {};
|
|
|
|
const submitInfoBase = {
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
ctpvNm: lastSearchedValues.sido ?? "",
|
|
sggNm:
|
|
lastSearchedValues.gugun === "ALL"
|
|
? ""
|
|
: (lastSearchedValues.gugun ?? ""),
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
laePicNm: submitInfo.laePicNm,
|
|
laePicTelNo: submitInfo.laePicTelNo,
|
|
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
|
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
|
laeIdfrNm: submitInfo.laeIdfrNm,
|
|
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
|
|
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
|
|
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
|
|
};
|
|
|
|
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,
|
|
false,
|
|
false,
|
|
);
|
|
};
|
|
|
|
try {
|
|
showGlobalProgress({ label: "서버에 요청 중...", progress: 0 });
|
|
const userInfo = await getUserInfo();
|
|
setGlobalProgressLabel("일반 검증 진행 중...");
|
|
|
|
let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean } };
|
|
|
|
if (isSewageTreatTab) {
|
|
const param: SewageTreatFactStatusSubmitRequest = {
|
|
...submitInfoBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: buildSewageTreatFactStatusSaveList(currentData),
|
|
...adminOpt,
|
|
};
|
|
response =
|
|
await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus(
|
|
param,
|
|
);
|
|
} else if (isSepticTankTab) {
|
|
const param: SepticTankStatusSubmitRequest = {
|
|
...submitInfoBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: buildSepticTankStatusSaveList(currentData),
|
|
...adminOpt,
|
|
};
|
|
response =
|
|
await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param);
|
|
} else {
|
|
const param: PrivateSewageDesigStatusSubmitRequest = {
|
|
...submitInfoBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: buildPrivateSewageDesigStatusSaveList(currentData),
|
|
...adminOpt,
|
|
};
|
|
response =
|
|
await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus(
|
|
param,
|
|
);
|
|
}
|
|
|
|
if (response.status !== "success") {
|
|
alert(response.message || "데이터 제출 중 오류가 발생했습니다.");
|
|
return;
|
|
}
|
|
|
|
const finalStatus = response.data?.finalStatus;
|
|
const nvpassed = response.data?.nvpassed;
|
|
|
|
if (finalStatus === "5") {
|
|
setGlobalProgress(50);
|
|
setGlobalProgressLabel("IMR 검증 진행 중...");
|
|
const imrParam = {
|
|
user_id: userInfo.userUnqId,
|
|
sewer_bzmn_cd: 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();
|
|
setShowSubmitDialog(false);
|
|
}
|
|
};
|
|
|
|
const saveFunc = () => {
|
|
void (async () => {
|
|
const tabKey = activeTabRef.current;
|
|
const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } =
|
|
resolvePrivateSewageFacilityTab(tabKey);
|
|
if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) {
|
|
alert("저장할 수 없는 탭입니다.");
|
|
return;
|
|
}
|
|
|
|
if (!lastSearchedValues || !searchParamRef.current) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (!(await confirm("저장 하시겠습니까?"))) return;
|
|
|
|
const currentData = getTabGridRows(tabKey);
|
|
if (!currentData.length) {
|
|
alert("저장할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const baseRequest = {
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
ctpvNm: lastSearchedValues.sido ?? "",
|
|
sggNm:
|
|
lastSearchedValues.gugun === "ALL"
|
|
? ""
|
|
: (lastSearchedValues.gugun ?? ""),
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
};
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const userInfo = await getUserInfo();
|
|
|
|
if (isSewageTreatTab) {
|
|
const param: SewageTreatFactStatusSaveRequest = {
|
|
...baseRequest,
|
|
userId: userInfo.userUnqId,
|
|
saveList: buildSewageTreatFactStatusSaveList(currentData),
|
|
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
};
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.saveSewageTreatFactStatus(
|
|
param,
|
|
);
|
|
if (response.status === "success") {
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
lastSearchedValues,
|
|
false,
|
|
false,
|
|
);
|
|
alert("저장되었습니다.");
|
|
} else {
|
|
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isSepticTankTab) {
|
|
const param: SepticTankStatusSaveRequest = {
|
|
...baseRequest,
|
|
userId: userInfo.userUnqId,
|
|
saveList: buildSepticTankStatusSaveList(currentData),
|
|
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
};
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.saveSepticTankStatus(param);
|
|
if (response.status === "success") {
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
lastSearchedValues,
|
|
false,
|
|
false,
|
|
);
|
|
alert("저장되었습니다.");
|
|
} else {
|
|
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
const param: PrivateSewageDesigStatusSaveRequest = {
|
|
...baseRequest,
|
|
userId: userInfo.userUnqId,
|
|
saveList: buildPrivateSewageDesigStatusSaveList(currentData),
|
|
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
|
};
|
|
const response =
|
|
await statisticsInputPrivateSewageFacilityApi.savePrivateSewageDesigStatus(
|
|
param,
|
|
);
|
|
if (response.status === "success") {
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
lastSearchedValues,
|
|
false,
|
|
false,
|
|
);
|
|
alert("저장되었습니다.");
|
|
} else {
|
|
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
|
|
}
|
|
} catch {
|
|
alert("데이터 저장 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
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 columnInfoByTab = useMemo(() => {
|
|
const map = new Map<string, ColumnInfo>();
|
|
for (const tab of PrivateSewageFacilityInpTabList) {
|
|
const tabKey = tab.tabKey;
|
|
const canEdit =
|
|
computeTabEnterable(
|
|
tabStatusInfoMap.get(tabKey) ?? null,
|
|
inputPeriodEnterable,
|
|
) || isAdminInfo;
|
|
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
|
|
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 activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
|
const activeTabToolbar =
|
|
toolbarSnapshotByTab.get(activeTab) ??
|
|
computeTabToolbarSnapshot(
|
|
activeTabStatus,
|
|
tabRowDataExists(rowDataMap, activeTab),
|
|
hasSearched,
|
|
lastSearchedValues,
|
|
inputPeriodEnterable,
|
|
isAdminInfo,
|
|
);
|
|
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } =
|
|
activeTabToolbar;
|
|
|
|
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"
|
|
? "IMR검증 - "
|
|
: e.vrfcCd === "NV"
|
|
? "일반검증 - "
|
|
: "HB검증 - ";
|
|
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
|
})
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<SearchForm
|
|
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}
|
|
/>
|
|
{showEnterableToolbarButtons && (
|
|
<>
|
|
<LastYearDataButton
|
|
getLastYearDataFunc={() => void lastYearDataFunc()}
|
|
/>
|
|
<SubmitButton submitFunc={submitFunc} />
|
|
</>
|
|
)}
|
|
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
|
|
<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>
|
|
{PrivateSewageFacilityInpTabList.map((info) => (
|
|
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
|
{info.tabLabel || info.tabKey}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{PrivateSewageFacilityInpTabList.map((info) => {
|
|
const tabKey = info.tabKey;
|
|
const rowData = rowDataMap.get(tabKey) ?? [];
|
|
const tabEnterableForGrid =
|
|
computeTabEnterable(
|
|
tabStatusInfoMap.get(tabKey) ?? null,
|
|
inputPeriodEnterable,
|
|
) || isAdminInfo;
|
|
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)
|
|
}
|
|
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;
|
|
});
|
|
}}
|
|
showHeaderCheckbox={false}
|
|
fixedColumns={info.fixedColumns}
|
|
enableSort={false}
|
|
statusInfo={tabStatusInfo}
|
|
isExcelImport={
|
|
tabRowDataExists(rowDataMap, tabKey) &&
|
|
tabEnterableForGrid &&
|
|
gugunOk
|
|
}
|
|
isExcelExport
|
|
cellStyleCallback={verificationCellStyle}
|
|
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,
|
|
false,
|
|
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 PrivateSewageFacilityInp;
|