import { useCallback, useEffect, useMemo, useRef, useState, type RefObject, } from "react"; import { flushSync } from "react-dom"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { ValueType } from "realgrid"; import type { RealGridReact } from "realgrid-react"; import { LoadingWithAnimation } from "@common/components"; 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 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 { PumpingStationInpColumn } from "@/components/util/column/input/PumpingStationInpColumn"; import SubmitDailog from "./SubmitDailog"; import type { CellStyleCallbackParams, CellTooltipCallbackParams, ColumnConfig, } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { doList } from "@/components/util/column/input/DoList"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag"; import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-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 { PUMPING_STATION_STATS_CD, type PumpingStationItem, type PumpingStationRequestParams, type PumpingStationSaveItem, type PumpingStationVerificationResult, } from "@/entities/statistics/model/input/statistics-input-pumping-station-interface"; import type { ApiResponse, cancelSubmitRequest, ImrStatusResponse, Sgg, SewerageStatusInfo, } from "@/entities/statistics/model/statistics-interface"; 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 type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; type GridRowWithVerification = Record & { verificationResult?: PumpingStationVerificationResult; }; type TabToolbarSnapshot = { showSaveInToolbar: boolean; showEnterableToolbarButtons: boolean; showActionToolbarCard: boolean; }; const computeTabEnterable = ( statusInfo: SewerageStatusInfo | null | undefined, periodEnterable: boolean, ): boolean => { if (statusInfo?.laeSbmsnSttsCd) { if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { return false; } return periodEnterable; } return periodEnterable; }; function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { return doList.includes(values?.bsnmCd || "") ? Boolean(values?.gugun && values.gugun !== "ALL") : true; } function computeTabToolbarSnapshot( statusInfo: SewerageStatusInfo | null, hasRowData: boolean, treatAsSearched: boolean, searchContext: SearchFormValues | null, periodEnterable: boolean, isAdminInfo: boolean, ): TabToolbarSnapshot { const gugunOk = buildGugunOkForToolbar(searchContext); const enterable = computeTabEnterable(statusInfo, periodEnterable); const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showActionToolbarCard = (hasRowData || treatAsSearched) && (showSaveInToolbar || showEnterableToolbarButtons || showSubmittedInInputPeriodToolbar || (isSubmitted && isAdminInfo)); return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; } function buildPumpingStationSearchParam( values: SearchFormValues, overrides?: { mode?: string }, ): PumpingStationRequestParams { const fcltNm = values.fctName?.trim(); return { statsYr: values.year, sewerBzmnCd: values.bsnmCd, ctpvNm: values.sido, sggNm: values.gugun === "ALL" ? "" : values.gugun, ...(fcltNm ? { fcltNm } : {}), ...(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 mapPumpingStationListToGridRows( list: PumpingStationItem[], ctpvNm: string, ): GridRowWithVerification[] { return list.map((item) => { const row = toGridRows([ { ...item, ctpvNm, oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), }, ])[0] as GridRowWithVerification; if (item.verificationResult) { row.verificationResult = item.verificationResult; } return row; }); } 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 resolvePumpingStationAdmdst( 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 }; } /** 그리드 행 → ST00050001 저장/삭제 API 항목 */ function buildPumpingStationSaveItem( row: Record, sggOptions: Sgg[] = [], ): PumpingStationSaveItem { const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions); const fcltSn = String(row.fcltSn ?? "").trim(); return { admdstCd, admdstNm, fcltSn: fcltSn, swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""), fcltNm: String(row.fcltNm ?? ""), pumpSeNm: String(row.pumpSeNm ?? ""), fcltPstnCn: String(row.fcltPstnCn ?? ""), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), planMltvArea: toGridNumber(row.planMltvArea), sndyArea: toGridNumber(row.sndyArea), rainArea: toGridNumber(row.rainArea), drtwPumpN1DmtrArea: toGridNumber(row.drtwPumpN1DmtrArea), drtwPumpN1Ppqty: toGridNumber(row.drtwPumpN1Ppqty), drtwPumpN1Cnt: toGridNumber(row.drtwPumpN1Cnt), drtwPumpN1RsrvCnt: toGridNumber(row.drtwPumpN1RsrvCnt), drtwPumpN2DmtrArea: toGridNumber(row.drtwPumpN2DmtrArea), drtwPumpN2Ppqty: toGridNumber(row.drtwPumpN2Ppqty), drtwPumpN2Cnt: toGridNumber(row.drtwPumpN2Cnt), drtwPumpN2RsrvCnt: toGridNumber(row.drtwPumpN2RsrvCnt), drtwPumpN3DmtrArea: toGridNumber(row.drtwPumpN3DmtrArea), drtwPumpN3Ppqty: toGridNumber(row.drtwPumpN3Ppqty), drtwPumpN3Cnt: toGridNumber(row.drtwPumpN3Cnt), drtwPumpN3RsrvCnt: toGridNumber(row.drtwPumpN3RsrvCnt), exlnPumpN1DmtrArea: toGridNumber(row.exlnPumpN1DmtrArea), exlnPumpN1Ppqty: toGridNumber(row.exlnPumpN1Ppqty), exlnPumpN1Cnt: toGridNumber(row.exlnPumpN1Cnt), exlnPumpN1RsrvCnt: toGridNumber(row.exlnPumpN1RsrvCnt), exlnPumpN2DmtrArea: toGridNumber(row.exlnPumpN2DmtrArea), exlnPumpN2Ppqty: toGridNumber(row.exlnPumpN2Ppqty), exlnPumpN2Cnt: toGridNumber(row.exlnPumpN2Cnt), exlnPumpN2RsrvCnt: toGridNumber(row.exlnPumpN2RsrvCnt), exlnPumpN3DmtrArea: toGridNumber(row.exlnPumpN3DmtrArea), exlnPumpN3Ppqty: toGridNumber(row.exlnPumpN3Ppqty), exlnPumpN3Cnt: toGridNumber(row.exlnPumpN3Cnt), exlnPumpN3RsrvCnt: toGridNumber(row.exlnPumpN3RsrvCnt), anlElpwrUseQnt: toGridNumber(row.anlElpwrUseQnt), rmrk: nullableTrimmedString(row.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("시계열 검증 완료 대기 시간이 초과되었습니다."); } function createEmptyPumpingStationRow( ctpvNm: string, sggNm = "", ): GridRowWithVerification { const row: GridRowWithVerification = { ctpvNm, sggNm }; for (const col of PumpingStationInpColumn.columns) { const fieldName = col.fieldName; if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; } return row; } const PUMPING_STATION_FACILITY_NAME_GUIDE_MESSAGE = "펌프장 등록시 시설명이 동일할 경우 같은 데이터로 분류 됩니다.\n\n" + "시설명이 동일 할 경우 구분을 위해 펌프장 1, 펌프장 2 형태로 구분하여 주시기 바랍니다."; // 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴) const PumpingStationInp = () => { 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); useEffect(() => { alert(PUMPING_STATION_FACILITY_NAME_GUIDE_MESSAGE); }, [alert]); const [rowData, setRowData] = useState[]>([]); const [isNotApplicable, setIsNotApplicable] = useState(false); const [searchValues, setSearchValues] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); const [hasSearched, setHasSearched] = useState(false); const [statusInfo, setStatusInfo] = useState(null); const [statsCd, setStatsCd] = useState(""); const [isVerificationFailed, setIsVerificationFailed] = useState(false); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [isLoading, setIsLoading] = useState(false); const [sggOptions, setSggOptions] = useState([]); const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState< Record >({}); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [pumpingStationDeleteList, setPumpingStationDeleteList] = useState< Record[] >([]); const gridRef = useRef(null); const gridGetRowsRef = useRef<(() => Record[]) | null>(null); const sggOptionsSidoRef = useRef(null); const [searchLoaded, setSearchLoaded] = useState(false); useEffect(() => { let cancelled = false; void (async () => { try { const res = await statisticsApi.getColumnGuides(PUMPING_STATION_STATS_CD); if (cancelled) return; if (res.status !== "success" || !Array.isArray(res.data)) { setColumnGuidesByEngCol({}); return; } 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; } setColumnGuidesByEngCol(next); } catch { if (!cancelled) setColumnGuidesByEngCol({}); } })(); 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 columnInfo = useMemo(() => { const enterable = computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; const rawColumns = PumpingStationInpColumn.columns ?? []; const base = enterable ? rawColumns : rawColumns.map((col) => ({ ...col, editable: false as const })); const columns = base .map((col: ColumnConfig) => { if (col.fieldName === "ctpvNm") { return { ...col, editable: false as const }; } if (col.fieldName === "sggNm" && enterable) { return buildSggNmColumn(col, true, sggOptions); } return col; }) .map((col) => { const guide = columnGuidesByEngCol[col.fieldName]?.trim(); return guide ? { ...col, tooltip: guide } : col; }); return { columns, columnLayout: PumpingStationInpColumn.columnLayout ?? [], }; }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); const canEditGrid = computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; const getCurrentRows = (): Record[] => gridGetRowsRef.current?.() ?? rowData; const commitGrid = () => { const gridView = gridRef.current?.gridView as | { commit?: (force?: boolean) => void; cancel?: () => void } | undefined; try { gridView?.commit?.(true); } catch { gridView?.cancel?.(); } }; const fetchPumpingStationList = async ( values: SearchFormValues, overrides?: { mode?: string }, ): Promise => { const response = await statisticsInputPumpingStationApi.getPumpingStationList( buildPumpingStationSearchParam(values, overrides), ); if (response.status !== "success" || !response.data) { alert(response.message ?? "데이터 조회 중 오류가 발생했습니다."); setSearchLoaded(false); return false; } const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } = response.data; const list = pumpingStationList ?? []; const gridRows = mapPumpingStationListToGridRows(list, commonInfo.ctpvNm); setPumpingStationDeleteList([]); setRowData(gridRows); setStatusInfo(nextStatusInfo); setStatsCd(commonInfo.statsCd); setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); if (overrides?.mode !== "PRE") { setIsNotApplicable(gridRows.length === 0); } else if (gridRows.length > 0) { setIsNotApplicable(false); } setSearchLoaded(true); return true; }; const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === "ALL") { alert("시도를 선택해주세요."); return; } try { const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( values.year || "", ); if (periodResponse.status !== "success") { alert(periodResponse.message); return; } const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; setInputPeriodEnterable(isInputPeriodCurrent(periods)); setIsLoading(true); setHasSearched(true); setLastSearchedValues(values); const loadedSgg = await loadSggOptions(values.sido); sggOptionsSidoRef.current = values.sido; if (loadedSgg.length === 0) { alert("시군구 목록을 불러오지 못했습니다."); setSearchLoaded(false); return; } await fetchPumpingStationList(values); } catch { alert("데이터 조회 중 오류가 발생했습니다."); setRowData([]); setStatusInfo(null); setSearchLoaded(false); } finally { setIsLoading(false); } }; const handleReset = () => { setPumpingStationDeleteList([]); setRowData([]); setLastSearchedValues(null); setHasSearched(false); setStatusInfo(null); setStatsCd(""); setIsVerificationFailed(false); setIsNotApplicable(false); setInputPeriodEnterable(true); setSggOptions([]); sggOptionsSidoRef.current = null; setSearchLoaded(false); }; const getLastYearDataFunc = async () => { const values = lastSearchedValues ?? searchValues; if (!values?.sido || values.sido === "ALL") { alert("시도를 선택해주세요."); return; } if (!hasSearched || !searchLoaded) { alert("먼저 데이터를 조회해주세요."); return; } try { setIsLoading(true); await fetchPumpingStationList(values, { mode: "PRE" }); } catch { alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; const openVerificationHistoryDialog = () => { const ctx = lastSearchedValues ?? searchValues; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { alert("먼저 조회를 진행해주세요."); return; } setVerificationHistoryOpen(true); }; const cnacelSubmitFunc = () => { void (async () => { try { if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; if (!lastSearchedValues) { alert("먼저 조회를 진행해주세요."); return; } const userInfo = await getUserInfo(); const requestData: cancelSubmitRequest = { sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), statsYr: lastSearchedValues.year ?? "", mdfrId: userInfo.userUnqId, statsCd: statsCd || PUMPING_STATION_STATS_CD, }; const response = await statisticsApi.cancelSubmit(requestData); if (response.status === "success") { setIsLoading(true); await fetchPumpingStationList(lastSearchedValues); alert("제출 취소되었습니다."); } else { alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); } } catch { alert("제출 취소 중 오류가 발생했습니다."); } finally { setIsLoading(false); } })(); }; const submitFunc = () => { void (async () => { try { if (!lastSearchedValues) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } const noApplicable = isNotApplicable; const currentData = getCurrentRows(); if (!noApplicable && currentData.length === 0) { alert("해당사항 없음을 체크해주세요."); return; } if (await confirm("제출 하시겠습니까?")) { setShowSubmitDialog(true); } } catch { alert("제출 중 오류가 발생했습니다."); } })(); }; const saveFunc = () => { void (async () => { if (!lastSearchedValues) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } if (!(await confirm("저장 하시겠습니까?"))) return; commitGrid(); const noApplicable = isNotApplicable; const currentData = getCurrentRows(); const saveList = noApplicable ? [] : currentData.map((row) => buildPumpingStationSaveItem(row, sggOptions), ); const deleteList = pumpingStationDeleteList .filter((row) => String(row.fcltSn ?? "").trim() !== "") .map((row) => buildPumpingStationSaveItem(row, sggOptions)); if (!noApplicable && saveList.length === 0) { alert("해당사항 없음을 체크해주세요."); return; } try { setIsLoading(true); const userInfo = await getUserInfo(); const response = await statisticsInputPumpingStationApi.savePumpingStationList({ statsYr: lastSearchedValues.year ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", userId: userInfo.userUnqId, ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), noApplicable, saveList, deleteList, }); if (response.status === "success") { setPumpingStationDeleteList([]); await fetchPumpingStationList(lastSearchedValues); alert("저장되었습니다."); } else { alert(response.message ?? "저장 중 오류가 발생했습니다."); } } catch { alert("저장 중 오류가 발생했습니다."); } finally { setIsLoading(false); } })(); }; const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { if (!lastSearchedValues) { alert("먼저 조회를 진행해주세요."); return; } if (!buildGugunOkForToolbar(lastSearchedValues)) { alert("시군구를 선택해주세요."); return; } commitGrid(); const noApplicable = isNotApplicable; const currentData = getCurrentRows(); const submitList = noApplicable ? [] : currentData.map((row) => buildPumpingStationSaveItem(row, sggOptions)); const deleteList = pumpingStationDeleteList .filter((row) => String(row.fcltSn ?? "").trim() !== "") .map((row) => buildPumpingStationSaveItem(row, sggOptions)); if (!noApplicable && submitList.length === 0) { alert("해당사항 없음을 체크해주세요."); return; } const statsCdForSubmit = statsCd || PUMPING_STATION_STATS_CD; const verificationQuery = { sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), statsYr: lastSearchedValues.year ?? "", statsCd: statsCdForSubmit, }; const reloadAfterSubmit = async () => { setPumpingStationDeleteList([]); await fetchPumpingStationList(lastSearchedValues); }; try { showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); const userInfo = await getUserInfo(); setGlobalProgressLabel("일반 검증 진행 중..."); const response = await statisticsInputPumpingStationApi.submitPumpingStationList({ statsYr: lastSearchedValues.year ?? "", ctpvNm: lastSearchedValues.sido ?? "", sggNm: lastSearchedValues.gugun === "ALL" ? "" : (lastSearchedValues.gugun ?? ""), sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", userId: userInfo.userUnqId, 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" } : {}), noApplicable, submitList, deleteList, }); 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: statsCdForSubmit, stats_yr: lastSearchedValues.year ?? "", lae_sbmsn_stts_cd: statusInfo?.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 addArticleFunc = async () => { const ctx = lastSearchedValues ?? searchValues; if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { 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; } commitGrid(); const currentRows = getCurrentRows(); const newRow = createEmptyPumpingStationRow(ctx.sido, sggNm); flushSync(() => { setSggOptions(list); setRowData([...currentRows, newRow]); setIsNotApplicable(false); }); }; const deleteArticleFunc = () => { commitGrid(); const currentRows = getCurrentRows(); if (currentRows.length === 0) { alert("삭제할 데이터가 없습니다."); return; } const gridView = gridRef.current?.gridView as | { getCheckedRows?: (refresh?: boolean) => number[]; getCheckedItems?: (refresh?: boolean) => number[]; } | undefined; const checkedIndexes = gridView?.getCheckedRows?.(false) ?? gridView?.getCheckedItems?.(false) ?? []; if (checkedIndexes.length === 0) { alert("삭제할 항목을 선택해주세요."); return; } const indexSet = new Set( checkedIndexes.filter( (idx) => Number.isInteger(idx) && idx >= 0 && idx < currentRows.length, ), ); const removedRows = currentRows.filter((_, idx) => indexSet.has(idx)); if (removedRows.length === 0) { alert("삭제할 항목을 선택해주세요."); return; } setPumpingStationDeleteList((prev) => [...prev, ...removedRows]); setRowData(currentRows.filter((_, idx) => !indexSet.has(idx))); }; const verificationCellStyle = ({ fieldName, row, }: CellStyleCallbackParams): 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, }: CellTooltipCallbackParams): 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 searchContextForToolbar = lastSearchedValues ?? searchValues; const toolbarSnapshot = computeTabToolbarSnapshot( statusInfo, rowData.length > 0, hasSearched, searchContextForToolbar, inputPeriodEnterable, isAdminInfo, ); const showSaveInToolbar = toolbarSnapshot.showSaveInToolbar && toolbarGroupAllowed; const showEnterableToolbarButtons = toolbarSnapshot.showEnterableToolbarButtons && toolbarGroupAllowed; const showActionToolbarCard = toolbarSnapshot.showActionToolbarCard && toolbarGroupAllowed; const showArticleToolbarCard = hasSearched && toolbarGroupAllowed && (showSaveInToolbar || showEnterableToolbarButtons); const showNotApplicable = searchLoaded && hasSearched && rowData.length === 0 && computeTabEnterable(statusInfo, inputPeriodEnterable); const tabEnterableForGrid = computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const verificationQueryContext = (() => { const ctx = lastSearchedValues ?? searchValues; 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 { verificationSearchDefaults, searchFormRemountKey } = useStatisticsInputPendingNavigation({ urlPath: STATISTICS_INPUT_URL_PATH.PUMPING_STATION, onAutoSearch: (values) => handleSearch(values), }); return ( <>
{showActionToolbarCard && (
{showEnterableToolbarButtons && ( <> void getLastYearDataFunc()} /> )} {statusInfo?.laeSbmsnSttsCd === "2" && ( )} {showSaveInToolbar && }
)} {showArticleToolbarCard && (
{showNotApplicable && ( )}
)} {isLoading && (
)} } columnInfo={columnInfo} rowStartNum={PumpingStationInpColumn.rowStartNum} fixedColumns={PumpingStationInpColumn.fixedColumns} rowData={rowData} setRowData={setRowData} getGridDataRef={gridGetRowsRef} showHeaderCheckbox statusInfo={statusInfo} isExcelImport={ searchLoaded && tabEnterableForGrid && gugunOk } excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} />
{ const values = lastSearchedValues ?? searchValues; if (!values) return; await fetchPumpingStationList(values); }} /> { const values = lastSearchedValues ?? searchValues; if (!values) return; await fetchPumpingStationList(values); }} /> ); }; export default PumpingStationInp;