import { useEffect, useMemo, useRef, useState } from "react"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import CancelSubmitButton from "@/components/button/CancelSubmitButton"; 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 VerificationResultButton from "@/components/button/VerificationResultButton"; import DataGrid from "@/components/module/DataGrid"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; 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 { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn"; import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-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 { SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, type SewerageServiceLevelComplaintItem, type SewerageServiceLevelComplaintRequestParams, type SewerageServiceLevelComplaintSaveItem, type SewerageServiceLevelResidentFriendlyFactItem, type SewerageServiceLevelResidentFriendlyFactSaveItem, type SewerageServiceLevelVerificationResult, } from "@/entities/statistics/model/input/statistics-input-sewerage-service-level-interface"; import type { SewerageStatusInfo, cancelSubmitRequest, ImrStatusResponse, } 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 SubmitDailog from "./SubmitDailog"; const SewerageServiceLevelInpTabList = [ ComplaintInpColumn, ResidentFriendlyFactInpColumn, ] as const; const firstTabKey = ComplaintInpColumn.tabKey; const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS = [ SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, ] as const; const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record = { [ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, [ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, }; const EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG = "06. 공공하수처리시설 데이터가 없으면 전년도 데이터 조회 및 항목 추가가 불가능 합니다."; function buildEmptyDataAlertOverrides( tabKey: string, ): { alertIfResidentFriendlyEmpty?: boolean } | undefined { if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { return { alertIfResidentFriendlyEmpty: true }; } return undefined; } type GridRowWithVerification = Record & { verificationResult?: SewerageServiceLevelVerificationResult; }; type TabToolbarSnapshot = { showSaveInToolbar: boolean; showEnterableToolbarButtons: boolean; showActionToolbarCard: boolean; }; const toGridColumnInfo = (tab: (typeof SewerageServiceLevelInpTabList)[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 buildSewerageServiceLevelSearchParam( values: SearchFormValues, overrides?: { mode?: string }, ): SewerageServiceLevelComplaintRequestParams { return { statsYr: values.year, sewerBzmnCd: values.bsnmCd, ctpvNm: values.sido, sggNm: values.gugun === "ALL" ? "" : values.gugun, ...(overrides?.mode ? { mode: overrides.mode } : {}), }; } function mapServiceLevelListToGridRows< T extends { verificationResult?: SewerageServiceLevelVerificationResult }, >(list: T[], ctpvNm: string): GridRowWithVerification[] { return list.map((item) => { const { verificationResult, ...rest } = item; const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; if (verificationResult) row.verificationResult = verificationResult; return row; }); } function normalizeGridString(value: unknown): string { if (value === null || value === undefined) return ""; return typeof value === "string" ? value : String(value); } function normalizeGridRmrk(value: unknown): string | null { if (value === null || value === undefined) return null; const s = typeof value === "string" ? value : String(value); return s.trim() === "" ? null : s; } function buildColumnFormatNumber( columns: { fieldName?: string; dataFormat?: string }[], ) { const columnFormatMap = new Map(); columns.forEach((col) => { if (col.fieldName && col.dataFormat) { columnFormatMap.set(col.fieldName, col.dataFormat); } }); return (value: string | number | null | undefined, fieldName: string): number => { if (value === null || value === undefined || value === "") return 0; const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; if (isNaN(num)) return 0; const format = columnFormatMap.get(fieldName); if (!format) return num; const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); }; } function buildComplaintSaveList( currentData: Record[], ): SewerageServiceLevelComplaintSaveItem[] { const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns); return currentData.map((d) => ({ admdstCd: normalizeGridString(d.admdstCd), admdstNm: normalizeGridString(d.admdstNm), swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"), dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"), odorCnt: formatNumber(d.odorCnt, "odorCnt"), ldsCnt: formatNumber(d.ldsCnt, "ldsCnt"), plltnAcdntCnt: formatNumber(d.plltnAcdntCnt, "plltnAcdntCnt"), swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"), sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"), etcCnt: formatNumber(d.etcCnt, "etcCnt"), rmrk: normalizeGridRmrk(d.rmrk), whol: formatNumber(d.whol, "whol"), crtrPpltnCvlcptEvntcsCnt: formatNumber( d.crtrPpltnCvlcptEvntcsCnt, "crtrPpltnCvlcptEvntcsCnt", ), })); } function buildResidentFriendlyFactSaveList( currentData: Record[], ): SewerageServiceLevelResidentFriendlyFactSaveItem[] { const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns); return currentData.map((d) => ({ admdstCd: normalizeGridString(d.admdstCd), admdstNm: normalizeGridString(d.admdstNm), swtrmtFcltCd: normalizeGridString(d.swtrmtFcltCd), swtrmtFcltNm: normalizeGridString(d.swtrmtFcltNm), fcltSn: normalizeGridString(d.fcltSn), tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"), brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"), childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"), bdySportsFcltArea: formatNumber(d.bdySportsFcltArea, "bdySportsFcltArea"), sportsFcltArea: formatNumber(d.sportsFcltArea, "sportsFcltArea"), pkltArea: formatNumber(d.pkltArea, "pkltArea"), eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"), wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"), etcCnt: formatNumber(d.etcCnt, "etcCnt"), rmrk: normalizeGridRmrk(d.rmrk), })); } 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 || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`); } await sleep(pollIntervalMs); } throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다."); } // 하수도 통계 관리 > 13. 하수도 서비스 수준 const SewerageServiceLevelInp = () => { const { setMessage: alert } = useAlertStore((state) => state); const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); const showGlobalProgress = useGlobalProgressStore((state) => state.show); const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const [activeTab, setActiveTab] = useState(firstTabKey); const [rowDataMap, setRowDataMap] = useState[]>>( new Map(), ); const [isNotApplicable, setIsNotApplicable] = useState(false); const [searchValues, setSearchValues] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); const [hasSearched, setHasSearched] = useState(false); const [searchLoaded, setSearchLoaded] = 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 searchParamRef = useRef(null); const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); const activeTabRef = useRef(activeTab); const getGridDataRef = useRef<(() => Record[]) | null>(null); /** ST00130002 당해 조회 결과 유무(공공하수처리시설 연동) — 전년도 조회 가능 여부 */ const publicSewageFacilitiesAvailableRef = useRef(false); activeTabRef.current = activeTab; useEffect(() => { let cancelled = false; void (async () => { const entries = await Promise.all( SEWERAGE_SERVICE_LEVEL_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(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], ), ), ); })(); return () => { cancelled = true; }; }, []); const commitToolbarSnapshotForLoadedTab = ( tabKey: string, statusInfo: SewerageStatusInfo | null, hasRowData: boolean, searchContext: SearchFormValues, ) => { setToolbarSnapshotByTab((prev) => { const next = new Map(prev); next.set( tabKey, computeTabToolbarSnapshot( statusInfo, hasRowData, searchDoneRef.current, searchContext, periodEnterableRef.current, isAdminInfo, ), ); return next; }); }; const applyRowMap = ( mergeIntoExistingMap: boolean, apply: (m: Map[]>) => 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: SewerageServiceLevelComplaintRequestParams, searchContext: SearchFormValues, mergeIntoExistingMap: boolean, updateLastSearchedValues: boolean, overrides?: { mode?: string; alertIfResidentFriendlyEmpty?: boolean }, ): Promise => { const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; const fromPreYear = overrides?.mode === "PRE"; if (tabKey === ComplaintInpColumn.tabKey) { const response = await statisticsInputSewerageServiceLevelApi.getComplaintList(requestParam); if (response.status !== "success" || !response.data) { alert(response.message || "데이터 조회 중 오류가 발생했습니다."); return false; } const { commonInfo, statusInfo, complaintList } = response.data; const gridRows = mapServiceLevelListToGridRows( (complaintList ?? []) as SewerageServiceLevelComplaintItem[], commonInfo.ctpvNm, ); applyRowMap(mergeIntoExistingMap, (m) => { m.set(ComplaintInpColumn.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); setSearchLoaded(true); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); return true; } if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { const response = await statisticsInputSewerageServiceLevelApi.getResidentFriendlyFactList( requestParam, ); if (response.status !== "success" || !response.data) { alert(response.message || "데이터 조회 중 오류가 발생했습니다."); return false; } const { commonInfo, statusInfo, residentFriendlyFactList } = response.data; const gridRows = mapServiceLevelListToGridRows( (residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[], commonInfo.ctpvNm, ); if (!fromPreYear) { publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; if (gridRows.length === 0 && overrides?.alertIfResidentFriendlyEmpty) { alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); } } applyRowMap(mergeIntoExistingMap, (m) => { m.set(ResidentFriendlyFactInpColumn.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); setSearchLoaded(true); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); if (!fromPreYear) { setIsNotApplicable(gridRows.length === 0); } else if (gridRows.length > 0) { setIsNotApplicable(false); } return true; } alert("해당 탭 조회 API가 없습니다."); return false; }; const getTabGridRows = (tabKey: string): Record[] => getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; 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); setSearchLoaded(false); setIsNotApplicable(false); setInputPeriodEnterable(true); searchParamRef.current = null; searchDoneRef.current = false; periodEnterableRef.current = true; publicSewageFacilitiesAvailableRef.current = false; }; 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()); setIsNotApplicable(false); 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 = buildSewerageServiceLevelSearchParam(values); searchParamRef.current = searchParam; await loadActiveTabData( tabForLoad, searchParam, values, !yearChangedFromLastSearch, true, buildEmptyDataAlertOverrides(tabForLoad), ); } catch { alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; const handleActiveTabChange = async (tabKey: string) => { const prevTabKey = activeTabRef.current; const latestRowsInPrevTab = getTabGridRows(prevTabKey); if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { setRowDataMap((prev) => { const next = new Map(prev); next.set(prevTabKey, latestRowsInPrevTab); return next; }); } activeTabRef.current = tabKey; setActiveTab(tabKey); setIsNotApplicable(false); if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; if (tabRowDataExists(rowDataMap, tabKey)) return; try { setIsLoading(true); await loadActiveTabData( tabKey, searchParamRef.current, lastSearchedValues, true, false, buildEmptyDataAlertOverrides(tabKey), ); } catch { alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; const getLastYearDataFunc = async () => { const values = lastSearchedValues ?? searchValues; if (!values?.sido || values.sido === "ALL") { alert("시도를 선택해주세요."); return; } if (!hasSearched) { alert("먼저 데이터를 조회해주세요."); return; } if ( activeTabRef.current === ResidentFriendlyFactInpColumn.tabKey && !publicSewageFacilitiesAvailableRef.current ) { alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); return; } try { setIsLoading(true); const param = buildSewerageServiceLevelSearchParam(values, { mode: "PRE" }); await loadActiveTabData( activeTabRef.current, param, values, true, false, { mode: "PRE" }, ); } catch { alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; const submitFunc = () => { void (async () => { if (!lastSearchedValues || !searchParamRef.current) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } const tabKey = activeTabRef.current; const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; const latestRows = getTabGridRows(tabKey); const noApplicable = isResidentTab && isNotApplicable; if (latestRows.length === 0) { if (isResidentTab && !noApplicable) { alert("해당사항 없음을 체크해주세요."); return; } if (!isResidentTab) { alert("제출할 데이터가 없습니다."); return; } } if (await confirm("제출 하시겠습니까?")) { setShowSubmitDialog(true); } })(); }; const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { const tabKey = activeTabRef.current; const isComplaintTab = tabKey === ComplaintInpColumn.tabKey; const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; if (!isComplaintTab && !isResidentTab) { alert("제출할 수 없는 탭입니다."); return; } if (!lastSearchedValues || !searchParamRef.current) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } const latestRows = getTabGridRows(tabKey); setRowDataMap((prev) => { const next = new Map(prev); next.set(tabKey, latestRows); return next; }); const noApplicable = isResidentTab && isNotApplicable; const complaintSubmitList = buildComplaintSaveList(latestRows); const residentSubmitList = noApplicable ? [] : buildResidentFriendlyFactSaveList(latestRows); if (isComplaintTab && complaintSubmitList.length === 0) { alert("제출할 데이터가 없습니다."); return; } if (isResidentTab && !noApplicable && residentSubmitList.length === 0) { alert("해당사항 없음을 체크해주세요."); return; } const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; const statsCdForTab = tabStatsCdMap.get(tabKey) ?? (isComplaintTab ? SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD : SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD); const submitInfoBase = { statsYr: lastSearchedValues.year ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", laePicNm: submitInfo.laePicNm, laePicTelNo: submitInfo.laePicTelNo, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, laeIdfrNm: submitInfo.laeIdfrNm, laeIdfrTelNo: submitInfo.laeIdfrTelNo, laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, }; const verificationQuery = { sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), statsYr: lastSearchedValues.year ?? "", statsCd: statsCdForTab, }; const reloadSubmittedTab = async () => { await loadActiveTabData( tabKey, searchParamRef.current!, lastSearchedValues, true, false, ); }; try { showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); const userInfo = await getUserInfo(); setGlobalProgressLabel("일반 검증 진행 중..."); const response = isComplaintTab ? await statisticsInputSewerageServiceLevelApi.submitComplaintList({ ...submitInfoBase, userId: userInfo.userUnqId, ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), submitList: complaintSubmitList, }) : await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({ ...submitInfoBase, userId: userInfo.userUnqId, ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), noApplicable, submitList: residentSubmitList, }); if (response.status !== "success") { alert(response.message || "데이터 제출 중 오류가 발생했습니다."); return; } const finalStatus = response.data?.finalStatus; const nvpassed = response.data?.nvpassed; if (finalStatus === "5") { setGlobalProgress(50); setGlobalProgressLabel("시계열 검증 진행 중..."); const imrParam = { user_id: userInfo.userUnqId, sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", stats_cd: statsCdForTab, stats_yr: lastSearchedValues.year ?? "", lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2", }; try { const imrResponse = await statisticsApi.callImrService(imrParam); if (imrResponse.job_id) { await waitForImrJobCompleted( imrResponse.job_id, (id) => statisticsApi.getImrStatus(id), { onProgress: (jobProgress) => { setGlobalProgress(50 + Math.round(jobProgress / 2)); }, }, ); } setGlobalProgress(100); await reloadSubmittedTab(); await openVerificationResultDialogIfNeeded( verificationQuery, setVerificationResultDialogOpen, { notify: alert }, ); } catch (e) { await reloadSubmittedTab(); await openVerificationResultDialogIfNeeded( verificationQuery, setVerificationResultDialogOpen, { notify: alert, notifySubmitComplete: false, }, ); const msg = e instanceof Error && e.message ? e.message : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; alert(msg); } } else { setGlobalProgress(100); await reloadSubmittedTab(); await openVerificationResultDialogIfNeeded( verificationQuery, setVerificationResultDialogOpen, { notify: alert }, ); } } catch { alert("데이터 제출 중 오류가 발생했습니다."); } finally { hideGlobalProgress(); } }; const saveFunc = () => { void (async () => { const tabKey = activeTabRef.current; if (!lastSearchedValues || !searchParamRef.current) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } if (!(await confirm("저장 하시겠습니까?"))) return; const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; const latestRows = getTabGridRows(tabKey); setRowDataMap((prev) => { const next = new Map(prev); next.set(tabKey, latestRows); return next; }); const noApplicable = isResidentTab && isNotApplicable; const baseRequest = { statsYr: lastSearchedValues.year ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", }; try { setIsLoading(true); const userInfo = await getUserInfo(); if (tabKey === ComplaintInpColumn.tabKey) { const saveList = buildComplaintSaveList(latestRows); if (saveList.length === 0) { alert("저장할 데이터가 없습니다."); return; } const response = await statisticsInputSewerageServiceLevelApi.saveComplaintList({ ...baseRequest, userId: userInfo.userUnqId, ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }); if (response.status !== "success") { alert(response.message || "데이터 저장 중 오류가 발생했습니다."); return; } } else if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { const saveList = noApplicable ? [] : buildResidentFriendlyFactSaveList(latestRows); if (!noApplicable && saveList.length === 0) { alert("해당사항 없음을 체크해주세요."); return; } const response = await statisticsInputSewerageServiceLevelApi.saveResidentFriendlyFactList( { ...baseRequest, userId: userInfo.userUnqId, ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), noApplicable, saveList, }, ); if (response.status !== "success") { alert(response.message || "데이터 저장 중 오류가 발생했습니다."); return; } } else { alert("저장할 수 없는 탭입니다."); return; } await loadActiveTabData( tabKey, searchParamRef.current, lastSearchedValues, true, false, ); alert("저장되었습니다."); } catch { alert("데이터 저장 중 오류가 발생했습니다."); } finally { setIsLoading(false); } })(); }; const cnacelSubmitFunc = () => { void (async () => { try { if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; if (!lastSearchedValues || !searchParamRef.current) { alert("먼저 조회를 진행해주세요."); return; } const userInfo = await getUserInfo(); const tabKey = activeTabRef.current; const statsCdForTab = tabStatsCdMap.get(tabKey); if (!statsCdForTab) { alert("먼저 조회를 진행해주세요."); return; } const requestData: cancelSubmitRequest = { sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), statsYr: lastSearchedValues.year ?? "", mdfrId: userInfo.userUnqId, statsCd: statsCdForTab, }; const response = await statisticsApi.cancelSubmit(requestData); if (response.status === "success") { setIsLoading(true); await loadActiveTabData( tabKey, searchParamRef.current, lastSearchedValues, true, 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 SewerageServiceLevelInpTabList) { const tabKey = tab.tabKey; const canEdit = computeTabEnterable( tabStatusInfoMap.get(tabKey) ?? null, inputPeriodEnterable, ) || isAdminInfo; const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const base = canEdit ? rawColumns : rawColumns.map((col) => ({ ...col, editable: false as const })); const columns = base.map((col) => { const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); return guide ? { ...col, tooltip: guide } : col; }); map.set(tabKey, { columns, columnLayout: tab.columnLayout ?? [], }); } return map; }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabRowData = rowDataMap.get(activeTab) ?? []; 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 showNotApplicableForResidentTab = activeTab === ResidentFriendlyFactInpColumn.tabKey && searchLoaded && hasSearched && activeTabRowData.length === 0 && computeTabEnterable(activeTabStatus, inputPeriodEnterable); const showSecondaryToolbarCard = hasSearched && toolbarGroupAllowed && (showSaveInToolbar || showEnterableToolbarButtons || showNotApplicableForResidentTab); 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" ? "시계열 검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - "; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; }) .filter(Boolean) .join("\n"); } }; const { verificationSearchDefaults, searchFormRemountKey } = useStatisticsInputPendingNavigation({ urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL, 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 && (
{showNotApplicableForResidentTab && ( )}
)} {isLoading && (
)}
void handleActiveTabChange(v)} className="w-full" variant="default" > {SewerageServiceLevelInpTabList.map((info) => ( {info.tabLabel || info.tabKey} ))} {SewerageServiceLevelInpTabList.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={false} fixedColumns={info.fixedColumns} enableSort={false} statusInfo={tabStatusInfo} isExcelImport={ tabRowDataExists(rowDataMap, tabKey) && tabEnterableForGrid && gugunOk } excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} isExcelExport cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} getGridDataRef={ activeTab === tabKey ? getGridDataRef : 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 SewerageServiceLevelInp;