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 = { [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([ 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 & { 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[]>, 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[] { 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, 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, ): boolean { const snField = getArticleRowSnField(tabKey); if (!snField) return false; return String(row[snField] ?? "").trim() !== ""; } function buildRecycledWaterStatusSaveItem( row: Record, 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, 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, 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, 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, ): 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((resolve) => setTimeout(resolve, ms)); async function waitForImrJobCompleted( jobId: string, getStatus: (id: string) => Promise, options?: { pollIntervalMs?: number; maxAttempts?: number; onProgress?: (jobProgress: number) => void; }, ): Promise { const pollIntervalMs = options?.pollIntervalMs ?? 2000; const maxAttempts = options?.maxAttempts ?? 150; for (let i = 0; i < maxAttempts; i++) { const res = await getStatus(jobId); options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); } await sleep(pollIntervalMs); } throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); } // 하수도 통계 관리 > 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(firstTabKey); const [rowDataMap, setRowDataMap] = useState[]>>( new Map(), ); const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( () => new Map(), ); const [searchValues, setSearchValues] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); const [hasSearched, setHasSearched] = useState(false); const [isLoading, setIsLoading] = useState(false); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [tabStatusInfoMap, setTabStatusInfoMap] = useState< Map >(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< Map >(new Map()); const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< Map >(new Map()); const [columnGuidesByTab, setColumnGuidesByTab] = useState< Record> >({}); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [sggOptions, setSggOptions] = useState([]); const [deleteListByTab, setDeleteListByTab] = useState< Map[]> >(() => new Map()); const searchParamRef = useRef(null); const sggOptionsSidoRef = useRef(null); const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); const activeTabRef = useRef(activeTab); const getGridDataRef = useRef<(() => Record[]) | null>(null); const getGridRef = useRef(null); const loadedTabKeysRef = useRef>(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 = {}; 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 => { 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[]>) => void, ) => { if (mergeIntoExistingMap) { setRowDataMap((prev) => { const next = new Map(prev); apply(next); return next; }); } else { const next = new Map[]>(); apply(next); setRowDataMap(next); } }; const loadActiveTabData = async ( tabKey: string, param: WaterReuseRequestParams, searchContext: SearchFormValues, mergeIntoExistingMap: boolean, updateLastSearchedValues: boolean, overrides?: { mode?: string }, ): Promise => { const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; const fromPreYear = overrides?.mode === "PRE"; const applyNotApplicableForLoadedTab = ( gridRows: readonly Record[], ) => { 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[] => 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): 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(); 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> = 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 | 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 | 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 ( <>
{showActionToolbarCard && (
{showEnterableToolbarButtons && ( <> void getLastYearDataFunc()} /> )} {activeTabStatus?.laeSbmsnSttsCd === "2" && ( )} {showSaveInToolbar && }
)} {showSecondaryToolbarCard && (
{showNotApplicableForActiveTab && ( )} {isPublicSewageTreatTab && showEnterableToolbarButtons && ( )} {isArticleEditTab && showEnterableToolbarButtons && ( <> )}
)} {isLoading && (
)}
void handleActiveTabChange(v)} className="w-full" variant="default" > {WaterReuseInpTabList.map((info) => ( {info.tabLabel || info.tabKey} ))} {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 ( { 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 } /> ); })}
{ const values = lastSearchedValues ?? searchValues; if (!values || !searchParamRef.current) return; await loadActiveTabData( activeTabRef.current, searchParamRef.current, values, true, false, ); }} /> { const values = lastSearchedValues ?? searchValues; if (!values || !searchParamRef.current) return; await loadActiveTabData( activeTabRef.current, searchParamRef.current, values, true, false, ); }} /> ); }; export default WaterReuseInp;