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

2259 lines
91 KiB
TypeScript

import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type Dispatch,
type SetStateAction,
} from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { flushSync } from "react-dom";
import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton";
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 { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PublicSewageTreatWaterReuseInpColumn } from "@/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn";
import { RainFallUtilFactStatusInpColumn } from "@/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn";
import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn";
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import {
WATER_REUSE_BUSINESS_APPR_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
type PublicSewageTreatWaterReuseItem,
type PublicSewageTreatWaterReuseSaveItem,
type RainFallUtilFactStatusItem,
type RainFallUtilFactStatusSaveItem,
type RecycledWaterStatusItem,
type RecycledWaterStatusSaveItem,
type TreatWaterReuseBusinessApprStatusItem,
type TreatWaterReuseBusinessApprStatusSaveItem,
type TreatWaterReuseDesignConstStatusItem,
type TreatWaterReuseDesignConstStatusSaveItem,
type WaterReuseRequestParams,
type WaterReuseVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-water-reuse-interface";
import type {
ApiResponse,
ImrStatusResponse,
Sgg,
SewerageStatusInfo,
cancelSubmitRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
const WaterReuseInpTabList = [
PublicSewageTreatWaterReuseInpColumn,
RecycledWaterStatusInpColumn,
RainFallUtilFactStatusInpColumn,
TreatWaterReuseDesignConstStatusInpColumn,
TreatWaterReuseBusinessApprStatusInpColumn,
] as const;
const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey;
const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_BUSINESS_APPR_STATS_CD,
] as const;
const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
[PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
[RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD,
[RainFallUtilFactStatusInpColumn.tabKey]: WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
[TreatWaterReuseDesignConstStatusInpColumn.tabKey]: WATER_REUSE_DESIGN_CONST_STATS_CD,
[TreatWaterReuseBusinessApprStatusInpColumn.tabKey]: WATER_REUSE_BUSINESS_APPR_STATS_CD,
};
const ARTICLE_EDIT_TAB_KEYS = new Set<string>([
RecycledWaterStatusInpColumn.tabKey,
RainFallUtilFactStatusInpColumn.tabKey,
TreatWaterReuseDesignConstStatusInpColumn.tabKey,
TreatWaterReuseBusinessApprStatusInpColumn.tabKey,
]);
const NO_APPLICABLE_TAB_KEYS = ARTICLE_EDIT_TAB_KEYS;
const WATER_REUSE_TAB_BY_KEY = new Map(
WaterReuseInpTabList.map((tab) => [tab.tabKey, tab]),
);
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: WaterReuseVerificationResult;
};
type TabToolbarSnapshot = {
showSaveInToolbar: boolean;
showEnterableToolbarButtons: boolean;
showActionToolbarCard: boolean;
};
const toGridColumnInfo = (tab: (typeof WaterReuseInpTabList)[number]): ColumnInfo => ({
columns: tab.columns ?? [],
columnLayout: tab.columnLayout ?? [],
});
function tabRowDataExists(
rowDataMap: Map<string, Record<string, string | number>[]>,
tabKey: string,
): boolean {
const rows = rowDataMap.get(tabKey);
return Array.isArray(rows) && rows.length > 0;
}
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 buildWaterReuseSearchParam(
values: SearchFormValues,
overrides?: { mode?: string },
): WaterReuseRequestParams {
return {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
...(overrides?.mode ? { mode: overrides.mode } : {}),
};
}
function parseSggListFromResponse(res: ApiResponse<Sgg[]>): Sgg[] {
if (res.status !== "success" || res.data == null) return [];
return Array.isArray(res.data) ? res.data : [];
}
function buildSggNmColumn(
col: ColumnConfig,
canEdit: boolean,
sggOptions: Sgg[],
): ColumnConfig {
if (!canEdit) {
return { ...col, editable: false as const };
}
if (sggOptions.length === 0) {
return { ...col, editable: true };
}
const labels = sggOptions.map((s) => s.gugun);
return {
...col,
editable: true,
editor: {
type: "dropdown",
domainOnly: true,
textReadOnly: true,
commitOnSelect: true,
dropDownWhenClick: true,
values: labels,
labels,
},
values: labels,
labels,
lookupDisplay: true,
};
}
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
function resolveArticleAdmdst(
row: Record<string, string | number>,
sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } {
const existingCd = String(row.admdstCd ?? "").trim();
const existingNm = String(row.admdstNm ?? "").trim();
if (existingCd && existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm };
}
const sggNm = String(row.sggNm ?? "").trim();
const matched = sggOptions.find((s) => s.gugun === sggNm);
if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
}
return { admdstCd: existingCd, admdstNm: existingNm || sggNm };
}
function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null {
if (
tabKey === RecycledWaterStatusInpColumn.tabKey ||
tabKey === RainFallUtilFactStatusInpColumn.tabKey
) {
return "fcltSn";
}
if (
tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey ||
tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey
) {
return "bplcSn";
}
return null;
}
function isPersistedArticleRow(
tabKey: string,
row: Record<string, string | number>,
): boolean {
const snField = getArticleRowSnField(tabKey);
if (!snField) return false;
return String(row[snField] ?? "").trim() !== "";
}
function buildRecycledWaterStatusSaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[],
): RecycledWaterStatusSaveItem {
const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions);
return {
admdstCd,
admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(),
fcltNm: String(row.fcltNm ?? "").trim(),
bldgSeCn: String(row.bldgSeCn ?? "").trim(),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(),
cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
archWholArea: toGridNumber(row.archWholArea),
prcsCpct: toGridNumber(row.prcsCpct),
wtrUseQnt: toGridNumber(row.wtrUseQnt),
ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt),
sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt),
mnUsgCn: String(row.mnUsgCn ?? "").trim(),
ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
function buildRainFallUtilFactStatusSaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[],
): RainFallUtilFactStatusSaveItem {
const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions);
return {
admdstCd,
admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(),
fcltNm: String(row.fcltNm ?? "").trim(),
bdstTypCn: String(row.bdstTypCn ?? "").trim(),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(),
cmptYmd: toCompactYmdOrNull(row.cmptYmd),
instlAmt: toGridNumber(row.instlAmt),
rcptMynCn: String(row.rcptMynCn ?? "").trim(),
filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(),
impdCpct: toGridNumber(row.impdCpct),
anlUseQnt: toGridNumber(row.anlUseQnt),
exlnUtlzUsgExpln: String(row.exlnUtlzUsgExpln ?? "").trim(),
anlOperAmt: toGridNumber(row.anlOperAmt),
rmrk: nullableTrimmedString(row.rmrk),
};
}
function buildTreatWaterReuseDesignConstStatusSaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[],
): TreatWaterReuseDesignConstStatusSaveItem {
const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions);
return {
admdstCd,
admdstNm,
bplcSn: String(row.bplcSn ?? "").trim(),
bplcNm: String(row.bplcNm ?? "").trim(),
rprsvNm: String(row.rprsvNm ?? "").trim(),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(),
coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk),
};
}
function buildTreatWaterReuseBusinessApprStatusSaveItem(
row: Record<string, string | number>,
sggOptions: Sgg[],
): TreatWaterReuseBusinessApprStatusSaveItem {
const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions);
return {
admdstCd,
admdstNm,
bplcSn: String(row.bplcSn ?? "").trim(),
bplcNm: String(row.bplcNm ?? "").trim(),
rprsvNm: String(row.rprsvNm ?? "").trim(),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(),
coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk),
};
}
function buildPublicSewageTreatWaterReuseSaveItem(
row: Record<string, string | number>,
): PublicSewageTreatWaterReuseSaveItem {
return {
admdstCd: String(row.admdstCd ?? "").trim(),
admdstNm: String(row.admdstNm ?? row.sggNm ?? "").trim(),
fcltSn: String(row.fcltSn ?? "").trim(),
swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(),
wholPrcsQnt: toGridNumber(row.wholPrcsQnt),
lglYn: String(row.lglYn ?? "").trim(),
instlYn: String(row.instlYn ?? "").trim(),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd),
rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct),
mtcnsNm: String(row.mtcnsNm ?? "").trim(),
ruswtPplLen: toGridNumber(row.ruswtPplLen),
ruswtPumpLen: toGridNumber(row.ruswtPumpLen),
rmrk: nullableTrimmedString(row.rmrk),
rtlztnRt: toGridNumber(row.rtlztnRt),
};
}
function createEmptyArticleRow(
tabKey: string,
ctpvNm: string,
sggNm = "",
): GridRowWithVerification {
const row: GridRowWithVerification = { ctpvNm, sggNm };
const columns = (WATER_REUSE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[];
for (const col of columns) {
const fieldName = col.fieldName;
if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue;
row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : "";
}
return row;
}
function mapPublicSewageTreatWaterReuseListToGridRows(
list: PublicSewageTreatWaterReuseItem[],
ctpvNm: string,
): GridRowWithVerification[] {
return list.map((item) => {
const { verificationResult, ...rest } = item;
const row = toGridRows([
{
ctpvNm,
...rest,
sncnYmd: formatCompactYmdForGrid(item.sncnYmd),
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
instlAprvYmd: formatCompactYmdForGrid(item.instlAprvYmd),
},
])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult;
return row;
});
}
function mapRecycledWaterStatusListToGridRows(
list: RecycledWaterStatusItem[],
ctpvNm: string,
): GridRowWithVerification[] {
return list.map((item) => {
const { verificationResult, ...rest } = item;
const row = toGridRows([
{
ctpvNm,
...rest,
cmptnStrtYmd: formatCompactYmdForGrid(item.cmptnStrtYmd),
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
},
])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult;
return row;
});
}
function mapRainFallUtilFactStatusListToGridRows(
list: RainFallUtilFactStatusItem[],
ctpvNm: string,
): GridRowWithVerification[] {
return list.map((item) => {
const { verificationResult, ...rest } = item;
const row = toGridRows([
{
ctpvNm,
...rest,
cmptYmd: formatCompactYmdForGrid(item.cmptYmd),
},
])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult;
return row;
});
}
function mapTreatWaterReuseStatusListToGridRows<
T extends { coRegYmd: string; verificationResult?: WaterReuseVerificationResult },
>(list: T[], ctpvNm: string): GridRowWithVerification[] {
return list.map((item) => {
const { verificationResult, ...rest } = item;
const row = toGridRows([
{
ctpvNm,
...rest,
coRegYmd: formatCompactYmdForGrid(item.coRegYmd),
},
])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult;
return row;
});
}
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 검증 완료 대기 시간이 초과되었습니다.");
}
// 하수도 통계 관리 > 10. 물 재이용
const WaterReuseInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore();
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
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 [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(
new Map(),
);
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const [isLoading, setIsLoading] = 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 [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>>
>({});
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
const [deleteListByTab, setDeleteListByTab] = useState<
Map<string, Record<string, string | number>[]>
>(() => new Map());
const searchParamRef = useRef<WaterReuseRequestParams | null>(null);
const sggOptionsSidoRef = useRef<string | null>(null);
const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true);
const activeTabRef = useRef(activeTab);
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
const getGridRef = useRef<RealGridReact | null>(null);
const loadedTabKeysRef = useRef<Set<string>>(new Set());
activeTabRef.current = activeTab;
useEffect(() => {
let cancelled = false;
void (async () => {
const entries = await Promise.all(
WATER_REUSE_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(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map(
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
);
})();
return () => {
cancelled = true;
};
}, []);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") {
setSggOptions([]);
return [];
}
try {
const res = await statisticsApi.getSggAllList(sido);
const list = parseSggListFromResponse(res);
setSggOptions(list);
return list;
} catch {
setSggOptions([]);
return [];
}
}, []);
const shouldShowNotApplicable = (
tabKey: string,
hasRows: boolean,
statusInfo: SewerageStatusInfo | null | undefined,
): boolean =>
NO_APPLICABLE_TAB_KEYS.has(tabKey) &&
hasSearched &&
!hasRows &&
computeTabEnterable(statusInfo, inputPeriodEnterable);
const setIsNotApplicableForTab = useCallback((tabKey: string, nextValue: boolean) => {
setIsNotApplicableByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, nextValue);
return next;
});
}, []);
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: WaterReuseRequestParams,
searchContext: SearchFormValues,
mergeIntoExistingMap: boolean,
updateLastSearchedValues: boolean,
overrides?: { mode?: string },
): Promise<boolean> => {
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
const fromPreYear = overrides?.mode === "PRE";
const applyNotApplicableForLoadedTab = (
gridRows: readonly Record<string, string | number>[],
) => {
if (!NO_APPLICABLE_TAB_KEYS.has(tabKey)) return;
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
};
if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
}
if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) {
const response =
await statisticsInputWaterReuseApi.getPublicSewageTreatWaterReuseList(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, publicSewageTreatWaterReuseList } = response.data;
const gridRows = mapPublicSewageTreatWaterReuseListToGridRows(
(publicSewageTreatWaterReuseList ?? []) as PublicSewageTreatWaterReuseItem[],
commonInfo.ctpvNm,
);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(PublicSewageTreatWaterReuseInpColumn.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),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
if (tabKey === RecycledWaterStatusInpColumn.tabKey) {
const response = await statisticsInputWaterReuseApi.getRecycledWaterStatusList(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, recycledWaterList } = response.data;
const gridRows = mapRecycledWaterStatusListToGridRows(
(recycledWaterList ?? []) as RecycledWaterStatusItem[],
commonInfo.ctpvNm,
);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(RecycledWaterStatusInpColumn.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),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) {
const response = await statisticsInputWaterReuseApi.getRainFallUtilFactStatusList(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, rainFallUtilFactStatusList } = response.data;
const gridRows = mapRainFallUtilFactStatusListToGridRows(
(rainFallUtilFactStatusList ?? []) as RainFallUtilFactStatusItem[],
commonInfo.ctpvNm,
);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(RainFallUtilFactStatusInpColumn.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),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) {
const response =
await statisticsInputWaterReuseApi.getTreatWaterReuseDesignConstStatusList(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, treatWaterReuseDesignConstStatusList } = response.data;
const gridRows = mapTreatWaterReuseStatusListToGridRows(
(treatWaterReuseDesignConstStatusList ??
[]) as TreatWaterReuseDesignConstStatusItem[],
commonInfo.ctpvNm,
);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(TreatWaterReuseDesignConstStatusInpColumn.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),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) {
const response =
await statisticsInputWaterReuseApi.getTreatWaterReuseBusinessApprStatusList(
requestParam,
);
if (response.status !== "success" || !response.data) {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
return false;
}
const { commonInfo, statusInfo, treatWaterReuseBusinessApprStatusList } =
response.data;
const gridRows = mapTreatWaterReuseStatusListToGridRows(
(treatWaterReuseBusinessApprStatusList ??
[]) as TreatWaterReuseBusinessApprStatusItem[],
commonInfo.ctpvNm,
);
applyRowMap(mergeIntoExistingMap, (m) => {
m.set(TreatWaterReuseBusinessApprStatusInpColumn.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),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
alert("해당 탭 조회 API가 없습니다.");
return false;
};
const getTabGridRows = (tabKey: string): Record<string, string | number>[] =>
tabKey === activeTabRef.current
? (getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? [])
: (rowDataMap.get(tabKey) ?? []);
const commitActiveGrid = () => {
const gridView = getGridRef.current?.gridView as
| { commit?: (force?: boolean) => void; cancel?: () => void }
| undefined;
try {
gridView?.commit?.(true);
} catch {
gridView?.cancel?.();
}
};
const isCheckedRow = (row: Record<string, string | number>): boolean => {
const v = row.__checked;
return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true";
};
const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => {
if (!getGridRef.current?.gridView) return [];
const gridView = getGridRef.current.gridView as {
getCheckedItems?: () => number[];
getCheckedRows?: () => number[];
};
const checked =
(typeof gridView.getCheckedItems === "function"
? gridView.getCheckedItems()
: typeof gridView.getCheckedRows === "function"
? gridView.getCheckedRows()
: []) ?? [];
return checked.filter(
(idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount,
);
};
const handleReset = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
setLastSearchedValues(null);
setHasSearched(false);
setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map());
setInputPeriodEnterable(true);
searchParamRef.current = null;
searchDoneRef.current = false;
periodEnterableRef.current = true;
loadedTabKeysRef.current.clear();
setSggOptions([]);
sggOptionsSidoRef.current = null;
};
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());
setToolbarSnapshotByTab(new Map());
setDeleteListByTab(new Map());
loadedTabKeysRef.current.clear();
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current;
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
const searchParam = buildWaterReuseSearchParam(values);
searchParamRef.current = searchParam;
const loadedSgg = await loadSggOptions(values.sido);
sggOptionsSidoRef.current = values.sido;
if (ARTICLE_EDIT_TAB_KEYS.has(tabForLoad) && loadedSgg.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
await loadActiveTabData(
tabForLoad,
searchParam,
values,
!yearChangedFromLastSearch,
true,
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const handleActiveTabChange = async (tabKey: string) => {
const prevTabKey = activeTabRef.current;
commitActiveGrid();
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 (loadedTabKeysRef.current.has(tabKey)) return;
if (tabRowDataExists(rowDataMap, tabKey)) return;
try {
setIsLoading(true);
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
true,
false,
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(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 = buildWaterReuseSearchParam(values, { mode: "PRE" });
await loadActiveTabData(
activeTabRef.current,
param,
values,
true,
false,
{ mode: "PRE" },
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const submitFunc = () => {
void (async () => {
const tabKey = activeTabRef.current;
const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey;
const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
if (!isPublicSewageTab && !isArticleTab) {
alert("제출 API 연동 준비 중입니다.");
return;
}
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
if (isPublicSewageTab) {
const currentData = getTabGridRows(tabKey);
if (currentData.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
} else {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
if (!noApplicable && currentData.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
}
if (await confirm("제출 하시겠습니까?")) {
setShowSubmitDialog(true);
}
})();
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const tabKey = activeTabRef.current;
const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey;
const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
if (!isPublicSewageTab && !isArticleTab) {
alert("제출 API 연동 준비 중입니다.");
return;
}
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
commitActiveGrid();
const submitRequestBase = {
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,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
let articleSggOptions: Sgg[] | null = null;
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab =
tabStatsCdMap.get(tabKey) ??
WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ??
tabKey;
const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab,
};
const reloadAfterSubmit = async () => {
if (isArticleTab) {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
if (articleSggOptions) {
setSggOptions(articleSggOptions);
}
}
const param =
searchParamRef.current ??
buildWaterReuseSearchParam(lastSearchedValues);
await loadActiveTabData(tabKey, param, 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 (isPublicSewageTab) {
const currentData = getTabGridRows(tabKey);
const submitList = currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
if (submitList.length === 0) {
alert("제출할 데이터가 없습니다.");
return;
}
response = await statisticsInputWaterReuseApi.submitPublicSewageTreatWaterReuse({
...submitRequestBase,
userId: userInfo.userUnqId,
noApplicable: false,
submitList,
});
} else {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
list = await loadSggOptions(sido);
sggOptionsSidoRef.current = sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
articleSggOptions = list;
const articleSubmitRequest = {
...submitRequestBase,
userId: userInfo.userUnqId,
...(noApplicable ? { noApplicable: true } : {}),
};
if (tabKey === RecycledWaterStatusInpColumn.tabKey) {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildRecycledWaterStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list));
if (!noApplicable && submitList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
response = await statisticsInputWaterReuseApi.submitRecycledWaterStatus({
...articleSubmitRequest,
submitList,
deleteList,
});
} else if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildRainFallUtilFactStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
if (!noApplicable && submitList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
response = await statisticsInputWaterReuseApi.submitRainFallUtilFactStatus({
...articleSubmitRequest,
submitList,
deleteList,
});
} else if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
if (!noApplicable && submitList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
response =
await statisticsInputWaterReuseApi.submitTreatWaterReuseDesignConstStatus(
{
...articleSubmitRequest,
submitList,
deleteList,
},
);
} else if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) {
const submitList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
if (!noApplicable && submitList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
response =
await statisticsInputWaterReuseApi.submitTreatWaterReuseBusinessApprStatus(
{
...articleSubmitRequest,
submitList,
deleteList,
},
);
} else {
alert("제출 API 연동 준비 중입니다.");
return;
}
}
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 reloadAfterSubmit();
await openVerificationResultDialogIfNeeded(
verificationQuery,
setVerificationResultDialogOpen,
{ notify: alert },
);
} catch (e) {
await reloadAfterSubmit();
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 reloadAfterSubmit();
await openVerificationResultDialogIfNeeded(
verificationQuery,
setVerificationResultDialogOpen,
{ notify: alert },
);
}
} catch {
alert("데이터 제출 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
};
const saveFunc = () => {
void (async () => {
const tabKey = activeTabRef.current;
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
commitActiveGrid();
const requestBase = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
const reloadTabAfterSave = async () => {
const param =
searchParamRef.current ??
buildWaterReuseSearchParam(lastSearchedValues);
await loadActiveTabData(
tabKey,
param,
lastSearchedValues,
false,
false,
);
};
if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) {
const currentData = getTabGridRows(tabKey);
if (currentData.length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response =
await statisticsInputWaterReuseApi.savePublicSewageTreatWaterReuse({
...requestBase,
userId: userInfo.userUnqId,
noApplicable: false,
saveList: currentData.map(buildPublicSewageTreatWaterReuseSaveItem),
});
if (response.status === "success") {
await reloadTabAfterSave();
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
return;
}
if (tabKey === RecycledWaterStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
list = await loadSggOptions(sido);
sggOptionsSidoRef.current = sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
const saveList = noApplicable
? []
: currentData.map((row) =>
buildRecycledWaterStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response = await statisticsInputWaterReuseApi.saveRecycledWaterStatus({
...requestBase,
userId: userInfo.userUnqId,
...(noApplicable ? { noApplicable: true } : {}),
saveList,
deleteList,
});
if (response.status === "success") {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
setSggOptions(list);
await reloadTabAfterSave();
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
return;
}
if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
list = await loadSggOptions(sido);
sggOptionsSidoRef.current = sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
const saveList = noApplicable
? []
: currentData.map((row) =>
buildRainFallUtilFactStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response =
await statisticsInputWaterReuseApi.saveRainFallUtilFactStatus({
...requestBase,
userId: userInfo.userUnqId,
...(noApplicable ? { noApplicable: true } : {}),
saveList,
deleteList,
});
if (response.status === "success") {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
setSggOptions(list);
await reloadTabAfterSave();
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
return;
}
if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
list = await loadSggOptions(sido);
sggOptionsSidoRef.current = sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
const saveList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
);
if (!noApplicable && saveList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response =
await statisticsInputWaterReuseApi.saveTreatWaterReuseDesignConstStatus(
{
...requestBase,
userId: userInfo.userUnqId,
...(noApplicable ? { noApplicable: true } : {}),
saveList,
deleteList,
},
);
if (response.status === "success") {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
setSggOptions(list);
await reloadTabAfterSave();
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
return;
}
if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
list = await loadSggOptions(sido);
sggOptionsSidoRef.current = sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
const saveList = noApplicable
? []
: currentData.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
const deleteList = (deleteListByTab.get(tabKey) ?? [])
.filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) =>
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
);
if (!noApplicable && saveList.length === 0) {
alert("해당사항 없음을 체크해주세요.");
return;
}
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response =
await statisticsInputWaterReuseApi.saveTreatWaterReuseBusinessApprStatus(
{
...requestBase,
userId: userInfo.userUnqId,
...(noApplicable ? { noApplicable: true } : {}),
saveList,
deleteList,
},
);
if (response.status === "success") {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, []);
return next;
});
setSggOptions(list);
await reloadTabAfterSave();
alert("저장되었습니다.");
} else {
alert(response.message ?? "저장 중 오류가 발생했습니다.");
}
} catch {
alert("저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}
})();
};
const updateOperateDataButtonFunc = () => {
void (async () => {
if (!lastSearchedValues) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!(await confirm("운영 데이터를 업데이트 하시겠습니까?"))) return;
try {
showGlobalProgress({ label: "운영 데이터 업데이트 중...", progress: 0 });
const userInfo = await getUserInfo();
const request = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId,
};
const response =
await statisticsInputWaterReuseApi.updateWaterReuseOperationData(request);
if (response.status !== "success") {
alert(response.message || "운영 데이터 업데이트 중 오류가 발생했습니다.");
return;
}
setGlobalProgress(50);
const param: WaterReuseRequestParams = {
statsYr: request.statsYr,
ctpvNm: request.ctpvNm,
sggNm: request.sggNm,
sewerBzmnCd: request.sewerBzmnCd,
};
await loadActiveTabData(
PublicSewageTreatWaterReuseInpColumn.tabKey,
param,
lastSearchedValues,
false,
false,
);
setGlobalProgress(100);
alert("운영 데이터가 업데이트되었습니다.");
} catch {
alert("운영 데이터 업데이트 중 오류가 발생했습니다.");
} finally {
hideGlobalProgress();
}
})();
};
const addArticleFunc = async () => {
const ctx = lastSearchedValues ?? searchValues;
if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") {
alert("먼저 조회를 진행해주세요.");
return;
}
const tabKey = activeTabRef.current;
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
alert("항목을 추가할 수 없는 탭입니다.");
return;
}
const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : "";
let list = sggOptions;
if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) {
list = await loadSggOptions(ctx.sido);
sggOptionsSidoRef.current = ctx.sido;
}
if (list.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
commitActiveGrid();
const currentRows = getTabGridRows(tabKey);
const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm);
flushSync(() => {
setSggOptions(list);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, [...currentRows, newRow]);
return next;
});
});
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx);
};
const deleteArticleFunc = () => {
const tabKey = activeTabRef.current;
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
alert("항목을 제외할 수 없는 탭입니다.");
return;
}
commitActiveGrid();
const currentRows = getTabGridRows(tabKey);
if (currentRows.length === 0) {
alert("삭제할 데이터가 없습니다.");
return;
}
const checkedIndexes = getCheckedItemIndexesFromGrid(currentRows.length);
const checkedIndexSet =
checkedIndexes.length > 0
? new Set(checkedIndexes)
: new Set(
currentRows
.map((row, idx) => (isCheckedRow(row) ? idx : -1))
.filter((idx) => idx >= 0),
);
const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx));
const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx));
if (removedRows.length === 0) {
alert("삭제할 항목을 선택해주세요.");
return;
}
const persistedRemovedRows = removedRows.filter((row) =>
isPersistedArticleRow(tabKey, row),
);
if (persistedRemovedRows.length > 0) {
setDeleteListByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, [...(prev.get(tabKey) ?? []), ...persistedRemovedRows]);
return next;
});
}
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, nextRows);
return next;
});
const ctx = lastSearchedValues ?? searchValues;
if (ctx) {
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
commitToolbarSnapshotForLoadedTab(
tabKey,
statusInfo,
nextRows.length > 0,
ctx,
);
}
};
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 openVerificationHistoryDialog = () => {
setVerificationHistoryOpen(true);
};
const columnInfoByTab = useMemo(() => {
const map = new Map<string, ColumnInfo>();
for (const tab of WaterReuseInpTabList) {
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) => {
let next: ColumnConfig = col;
if (col.fieldName === "ctpvNm") {
next = { ...col, editable: false as const };
}
if (
tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey &&
col.fieldName === "sggNm"
) {
next = {
...col,
editable: false as const,
styleName: "rg-cell-readonly",
};
} else if (
ARTICLE_EDIT_TAB_KEYS.has(tabKey) &&
col.fieldName === "sggNm"
) {
next = buildSggNmColumn(col, canEdit, sggOptions);
}
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
return guide ? { ...next, tooltip: guide } : next;
});
map.set(tabKey, {
columns,
columnLayout: tab.columnLayout ?? [],
});
}
return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot(
activeTabStatus,
tabRowDataExists(rowDataMap, activeTab),
hasSearched,
lastSearchedValues,
inputPeriodEnterable,
isAdminInfo,
);
const showSaveInToolbar =
activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed;
const showEnterableToolbarButtons =
activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard =
activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed;
const showSecondaryToolbarCard =
hasSearched &&
toolbarGroupAllowed &&
(showSaveInToolbar || showEnterableToolbarButtons);
const isPublicSewageTreatTab = activeTab === PublicSewageTreatWaterReuseInpColumn.tabKey;
const isArticleEditTab = ARTICLE_EDIT_TAB_KEYS.has(activeTab);
const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false;
const showNotApplicableForActiveTab =
loadedTabKeysRef.current.has(activeTab) &&
shouldShowNotApplicable(
activeTab,
tabRowDataExists(rowDataMap, activeTab),
activeTabStatus,
);
const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback(
(next) => {
const tabKey = activeTabRef.current;
const current = isNotApplicableByTab.get(tabKey) ?? false;
const resolved =
typeof next === "function"
? (next as (prevState: boolean) => boolean)(current)
: next;
setIsNotApplicableForTab(tabKey, resolved);
},
[isNotApplicableByTab, setIsNotApplicableForTab],
);
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"
? "IMR검증 - "
: e.vrfcCd === "NV"
? "일반검증 - "
: "HB검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
})
.filter(Boolean)
.join("\n");
}
};
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.WATER_REUSE,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
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>
)}
{showSecondaryToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showNotApplicableForActiveTab && (
<NotApplicableCheckButton
isCheck={isNotApplicableForActiveTab}
setIsCheck={setIsNotApplicableForActiveTab}
/>
)}
{isPublicSewageTreatTab && showEnterableToolbarButtons && (
<UpdateOperateDataButton
updateOperateDataButtonFunc={updateOperateDataButtonFunc}
/>
)}
{isArticleEditTab && showEnterableToolbarButtons && (
<>
<AddArticleButton addArticleFunc={addArticleFunc} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</>
)}
</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>
{WaterReuseInpTabList.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{WaterReuseInpTabList.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 nextRows =
typeof updater === "function"
? updater(cur)
: updater;
next.set(tabKey, nextRows);
return next;
});
}}
showHeaderCheckbox={
ARTICLE_EDIT_TAB_KEYS.has(tabKey) &&
tabEnterableForGrid
}
fixedColumns={info.fixedColumns}
enableSort={false}
statusInfo={tabStatusInfo}
isExcelImport={
tabRowDataExists(rowDataMap, tabKey) &&
tabEnterableForGrid &&
gugunOk
}
isExcelExport
cellStyleCallback={verificationCellStyle}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
}
getGridRef={
activeTab === tabKey ? getGridRef : 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,
);
}}
/>
<SubmitDailog
open={showSubmitDialog}
onOpenChange={setShowSubmitDialog}
statusInfo={activeTabStatus}
onConfirm={executeSubmit}
submitMode="normal"
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,
);
}}
/>
<ConfirmDialog isOpen={false} />
</>
);
};
export default WaterReuseInp;