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

1095 lines
42 KiB
TypeScript

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<string, string | number> & {
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[]>): 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<string, string | number>,
sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } {
const existingCd = String(row.admdstCd ?? "").trim();
const existingNm = String(row.admdstNm ?? "").trim();
if (existingCd && existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm };
}
const sggNm = String(row.sggNm ?? "").trim();
const matched = sggOptions.find((s) => s.gugun === sggNm);
if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
}
return { admdstCd: existingCd, admdstNm: existingNm || sggNm };
}
/** 그리드 행 → ST00050001 저장/삭제 API 항목 */
function buildPumpingStationSaveItem(
row: Record<string, string | number>,
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<void>((resolve) => setTimeout(resolve, ms));
async function waitForImrJobCompleted(
jobId: string,
getStatus: (id: string) => Promise<ImrStatusResponse>,
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<void> {
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
const maxAttempts = options?.maxAttempts ?? 150;
for (let i = 0; i < maxAttempts; i++) {
const res = await getStatus(jobId);
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `시계열 검증이 ${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<Record<string, string | number>[]>([]);
const [isNotApplicable, setIsNotApplicable] = useState(false);
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
const [lastSearchedValues, setLastSearchedValues] =
useState<SearchFormValues | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<
Record<string, string>
>({});
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
useState(false);
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
const [pumpingStationDeleteList, setPumpingStationDeleteList] = useState<
Record<string, string | number>[]
>([]);
const gridRef = useRef<RealGridReact | null>(null);
const gridGetRowsRef =
useRef<(() => Record<string, string | number>[]) | null>(null);
const sggOptionsSidoRef = useRef<string | null>(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<string, string> = {};
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<Sgg[]> => {
if (!sido || sido === "ALL") {
setSggOptions([]);
return [];
}
try {
const res = await statisticsApi.getSggAllList(sido);
const list = parseSggListFromResponse(res);
setSggOptions(list);
return list;
} catch {
setSggOptions([]);
return [];
}
}, []);
const 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<string, string | number>[] =>
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<boolean> => {
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 (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
/>
{showActionToolbarCard && (
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={isVerificationFailed}
/>
{showEnterableToolbarButtons && (
<>
<LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()}
/>
<SubmitButton submitFunc={submitFunc} />
</>
)}
{statusInfo?.laeSbmsnSttsCd === "2" && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
)}
{showArticleToolbarCard && (
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
{showNotApplicable && (
<NotApplicableCheckButton
isCheck={isNotApplicable}
setIsCheck={setIsNotApplicable}
/>
)}
<AddArticleButton addArticleFunc={addArticleFunc} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<DataGrid
key={canEditGrid ? "enterable" : "readonly"}
getGridRef={gridRef as RefObject<RealGridReact | null>}
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}
/>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
onExceptionResetSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values) return;
await fetchPumpingStationList(values);
}}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={verificationQueryContext}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values) return;
await fetchPumpingStationList(values);
}}
/>
<SubmitDailog
open={showSubmitDialog}
onOpenChange={setShowSubmitDialog}
statusInfo={statusInfo}
onConfirm={executeSubmit}
isLoading={isLoading}
/>
<ConfirmDialog isOpen={false} />
</>
);
};
export default PumpingStationInp;