891 lines
44 KiB
TypeScript
891 lines
44 KiB
TypeScript
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
|
|
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
|
import {
|
|
STATISTICS_INPUT_URL_PATH,
|
|
verificationSearchToFormValues,
|
|
} from "@/shared/lib/stats-cd-input-navigation";
|
|
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
|
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
|
import {
|
|
type CheckInputPeriodItem,
|
|
getVerificationPeriodActionBlockedMessage,
|
|
isInputPeriodCurrent,
|
|
type SewerageSupplyRateItem,
|
|
type SewerageSupplyRateRequestParams,
|
|
type SewerageSupplyRateSaveItem,
|
|
type SewerageSupplyRateSaveRequest,
|
|
type SewerageSupplyRateVerificationResult,
|
|
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
|
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
|
import type {
|
|
cancelSubmitRequest,
|
|
ImrStatusResponse,
|
|
SewerageStatusInfo,
|
|
SubmitExceptionRequest,
|
|
} from "@/entities/statistics/model/statistics-interface";
|
|
import { LoadingWithAnimation } from "@common/components";
|
|
|
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
|
|
|
import SubmitDailog from "./SubmitDailog";
|
|
|
|
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
|
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
|
import SaveButton from "@/components/button/SaveButton";
|
|
import SubmitButton from "@/components/button/SumitButton";
|
|
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
|
// import { DataGrid } from "@rsone/components";
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import SearchForm from "@/components/module/SearchForm";
|
|
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
|
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
|
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
|
import { doList } from "@/components/util/column/input/DoList";
|
|
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
|
import { getFooterSummary } from "@/components/util/grid/footerSummary";
|
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
|
import { useAdminStore } from "@/store/admin-store";
|
|
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
|
import { useTabStore } from "@/store/tab-store";
|
|
|
|
|
|
type GridRowWithVerification = Record<string, string | number> & {
|
|
verificationResult?: SewerageSupplyRateVerificationResult;
|
|
};
|
|
|
|
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
|
|
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
|
|
|
|
// const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
|
// if (!verification) return false;
|
|
// return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
|
// };
|
|
|
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
|
|
/** IMR 상태가 COMPLETED 이고 job_prog 가 100일 때까지 폴링 */
|
|
async function waitForImrJobCompleted(
|
|
jobId: string,
|
|
getStatus: (id: string) => Promise<ImrStatusResponse>,
|
|
options?: {
|
|
pollIntervalMs?: number;
|
|
maxAttempts?: number;
|
|
onProgress?: (jobProgress: number) => void;
|
|
},
|
|
): Promise<void> {
|
|
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
|
|
const maxAttempts = options?.maxAttempts ?? 150;
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const res = await getStatus(jobId);
|
|
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
|
|
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
|
|
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
|
|
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
|
}
|
|
await sleep(pollIntervalMs);
|
|
}
|
|
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
|
}
|
|
|
|
function getSewerageSupplyRateParams(
|
|
values: SearchFormValues,
|
|
overrides?: { yr?: string; demoYr?: string; mode?: string }
|
|
): SewerageSupplyRateRequestParams {
|
|
return {
|
|
ctpvNm: values.sido,
|
|
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
|
sewerBzmnCd: values.bsnmCd,
|
|
statsYr: overrides?.yr ?? values.year,
|
|
...(overrides?.mode && { mode: overrides.mode }),
|
|
};
|
|
}
|
|
|
|
function normalizeGridString(value: unknown): string | null {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
return value.trim() === "" ? null : value;
|
|
}
|
|
|
|
// 하수도 통계 관리 > 01. 하수도 보급률
|
|
const SeweragePenetrationRateInp = () => {
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
|
const [isEnterable, setIsEnterable] = useState<boolean>(true);
|
|
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
|
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
|
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
|
|
/** 마지막 조회 시 check-input-period 응답 (검증기간 여부 판단) */
|
|
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
|
const [showDailog, setShowDailog] = useState<boolean>(false);
|
|
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
|
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
|
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
|
|
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
|
|
const [statsCd, setStatsCd] = useState<string>('');
|
|
/** `engColNm` → `columnGuide` (헤더 hover) */
|
|
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
|
|
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
|
|
const { confirm } = useConfirm();
|
|
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
|
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
|
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
|
|
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
|
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
|
|
|
const postVerificationSearchRef = useRef<SearchFormValues | null>(null);
|
|
const [verificationSearchDefaults, setVerificationSearchDefaults] =
|
|
useState<SearchFormValues | null>(null);
|
|
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
|
|
|
|
const activeTabId = useTabStore((s) => s.activeTabId);
|
|
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
|
|
|
|
useLayoutEffect(() => {
|
|
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
|
|
return;
|
|
}
|
|
const pending = useStatisticsInputFocusStore
|
|
.getState()
|
|
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE);
|
|
if (!pending?.search) return;
|
|
const values = verificationSearchToFormValues(pending.search);
|
|
setVerificationSearchDefaults(values);
|
|
postVerificationSearchRef.current = values;
|
|
setSearchFormRemountKey((k) => k + 1);
|
|
}, [activeTabId, inputFocusNonce]);
|
|
|
|
useEffect(() => {
|
|
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
void (async () => {
|
|
try {
|
|
const res = await statisticsApi.getColumnGuides(SEWERAGE_PENETRATION_COLUMN_GUIDE_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;
|
|
};
|
|
}, [activeTabId]);
|
|
|
|
const { isAdminInfo } = useAdminStore();
|
|
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
|
|
|
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
|
|
|
const _footerFieldNames = ['sewerBzmnWholPpltnCnt', 'sewerBzmnDomplWholPpltnCnt', 'sewerBzmnFrgnrWholPpltnCnt', 'sewerBzmnArea'] as const;
|
|
const footerWithApiDefault = (fieldName: (typeof _footerFieldNames)[number], numberFormat: string) => ({
|
|
expression: 'sum' as const,
|
|
numberFormat,
|
|
valueCallback: (grid: Parameters<typeof getFooterSummary>[0], column: Parameters<typeof getFooterSummary>[1]) =>
|
|
(footerFromApi && footerFromApi[fieldName] != null ? footerFromApi[fieldName] : null) ?? getFooterSummary(grid, column),
|
|
});
|
|
|
|
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
|
|
|
|
const columnInfo = useMemo(() => {
|
|
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
|
|
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
|
|
|
|
const withHeaderTooltips = base.map((col) => {
|
|
const guide = columnGuidesByEngCol[col.fieldName]?.trim();
|
|
return {
|
|
...col,
|
|
tooltip: guide && guide.length > 0 ? guide : undefined,
|
|
};
|
|
});
|
|
|
|
return {
|
|
columnLayout,
|
|
columns: withHeaderTooltips.map((col) => {
|
|
if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') };
|
|
if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') };
|
|
if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') };
|
|
// 그리드 필드명은 refArea(행정참고자료면적). API commonInfo.sewerBzmnArea와 매핑
|
|
if (col.fieldName === 'refArea') return { ...col, footer: footerWithApiDefault('sewerBzmnArea', '#,##0.000') };
|
|
return col;
|
|
}),
|
|
};
|
|
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]);
|
|
|
|
const handleReset = () => {
|
|
setRowData([]);
|
|
setStatusInfo(null);
|
|
setPeriodCheckList([]);
|
|
setLastSearchedYear(null);
|
|
setLastSearchedValues(null);
|
|
setFooterFromApi(null);
|
|
setVerificationSearchDefaults(null);
|
|
setSearchFormRemountKey((k) => k + 1);
|
|
};
|
|
|
|
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await statisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params);
|
|
if (response.status === 'success' && response.data) {
|
|
const commonInfo = response.data.commonInfo;
|
|
const statusInfo = response.data.statusInfo;
|
|
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d: SewerageSupplyRateItem) => ({
|
|
...d,
|
|
ctpvNm: commonInfo.ctpvNm,
|
|
spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn,
|
|
swagePplPsplyRt: commonInfo.swagePplPsplyRt,
|
|
}));
|
|
setRowData(
|
|
convertedData.map((d) => {
|
|
const row = toGridRows([d])[0] as GridRowWithVerification;
|
|
if (d.verificationResult) row.verificationResult = d.verificationResult;
|
|
return row;
|
|
})
|
|
);
|
|
|
|
setStatusInfo(statusInfo);
|
|
setFooterFromApi(
|
|
commonInfo
|
|
? {
|
|
sewerBzmnWholPpltnCnt: commonInfo.sewerBzmnWholPpltnCnt,
|
|
sewerBzmnDomplWholPpltnCnt: commonInfo.sewerBzmnDomplWholPpltnCnt,
|
|
sewerBzmnFrgnrWholPpltnCnt: commonInfo.sewerBzmnFrgnrWholPpltnCnt,
|
|
sewerBzmnArea: commonInfo.sewerBzmnArea,
|
|
}
|
|
: null
|
|
);
|
|
setStatsCd(commonInfo.statsCd);
|
|
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
|
|
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
|
|
setIsEnterable(false);
|
|
} else {
|
|
setIsEnterable(isEnterable);
|
|
}
|
|
} else {
|
|
setIsEnterable(isEnterable);
|
|
}
|
|
|
|
} else {
|
|
alert(response.message);
|
|
}
|
|
} catch {
|
|
alert('데이터 조회 중 오류가 발생했습니다.');
|
|
setRowData([]);
|
|
setStatusInfo(null);
|
|
setFooterFromApi(null);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSearch = async (values: SearchFormValues) => {
|
|
if (values.sido === '' || values.sido === 'ALL') {
|
|
alert('시도를 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
|
|
|
|
if (response.status === 'success') {
|
|
const periods = Array.isArray(response.data) ? response.data : [];
|
|
setPeriodCheckList(periods);
|
|
const checkEnterable = isInputPeriodCurrent(periods);
|
|
setLastSearchedYear(values.year || null);
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable);
|
|
setLastSearchedValues(values);
|
|
} else {
|
|
alert(response.message);
|
|
}
|
|
} catch {
|
|
alert('데이터 조회 중 오류가 발생했습니다.');
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const values = postVerificationSearchRef.current;
|
|
if (!values) return;
|
|
postVerificationSearchRef.current = null;
|
|
void handleSearch(values);
|
|
|
|
}, [searchFormRemountKey]);
|
|
|
|
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
|
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
|
if (verificationBlocked) {
|
|
alert(verificationBlocked);
|
|
return;
|
|
}
|
|
const userInfo = await getUserInfo();
|
|
|
|
const params: SubmitExceptionRequest = {
|
|
...submitInfo,
|
|
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
|
ctpvNm: searchValues?.sido ?? '',
|
|
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
|
statsYr: searchValues?.year ?? '',
|
|
userId: userInfo.userUnqId,
|
|
statsCd: statsCd,
|
|
};
|
|
|
|
// if (isAdminInfo) {
|
|
// params.superAdmin = 'superAdmin';
|
|
// }
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
|
|
const response = await statisticsApi.submitException(params);
|
|
|
|
if (response.status === 'success') {
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
|
alert('제출되었습니다.');
|
|
} else {
|
|
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
|
|
}
|
|
} catch {
|
|
alert('데이터 제출 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const getLastYearDataFunc = async (values: SearchFormValues) => {
|
|
if (values.sido === '' || values.sido === 'ALL') {
|
|
alert('시도를 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
// 실제 조회된 연도가 없으면 실행 불가
|
|
if (!lastSearchedYear) {
|
|
alert('먼저 데이터를 조회해주세요.');
|
|
return;
|
|
}
|
|
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true);
|
|
};
|
|
|
|
const submitFunc = async (isException: boolean) => {
|
|
if (!isException) {
|
|
// const isError = rowData.filter((d) =>
|
|
// hasVerificationErrors((d as GridRowWithVerification).verificationResult)
|
|
// );
|
|
|
|
// if (isError.length > 0) {
|
|
// alert(`데이터에 오류가 있습니다.
|
|
// 수정 후 제출해주세요.`);
|
|
// return;
|
|
// }
|
|
if (await confirm('제출 하시겠습니까?')) {
|
|
setIsExceptionSubmitFlow(false);
|
|
setShowDailog(true);
|
|
}
|
|
} else {
|
|
if (await confirm('예외 제출 시 검증 결과에 오류가 있어도 제출 처리됩니다.')) {
|
|
setIsExceptionSubmitFlow(true);
|
|
setShowDailog(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const executeExceptionSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
|
await SubmitExceptionFunc(submitInfo);
|
|
};
|
|
|
|
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
|
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
|
if (verificationBlocked) {
|
|
alert(verificationBlocked);
|
|
return;
|
|
}
|
|
try {
|
|
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
|
|
const userInfo = await getUserInfo();
|
|
const currentData = getGridDataRef.current?.();
|
|
|
|
if (!currentData || currentData.length === 0) {
|
|
alert('제출할 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const columnFormatMap = new Map<string, string>();
|
|
|
|
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
|
|
if (col.fieldName && col.dataFormat) {
|
|
columnFormatMap.set(col.fieldName, col.dataFormat);
|
|
}
|
|
});
|
|
|
|
const formatNumber = (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);
|
|
};
|
|
|
|
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
|
sggNm: normalizeGridString(d.sggNm),
|
|
admdstCd: normalizeGridString(d.admdstCd),
|
|
admdstNm: normalizeGridString(d.admdstNm),
|
|
trbtrNm: normalizeGridString(d.trbtrNm),
|
|
spcCtpnRgnCn:
|
|
d.spcCtpnRgnCn === '해당없음' ? null : normalizeGridString(d.spcCtpnRgnCn),
|
|
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
|
|
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
|
|
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
|
|
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
|
|
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
|
|
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
|
|
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'),
|
|
staaArea: formatNumber(d.staaArea, 'staaArea'),
|
|
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'),
|
|
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
|
|
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
|
|
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
|
|
rmrk: normalizeGridString(d.rmrk),
|
|
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
|
|
wholArea: formatNumber(d.wholArea, 'wholArea'),
|
|
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
|
|
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'),
|
|
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'),
|
|
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'),
|
|
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'),
|
|
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'),
|
|
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'),
|
|
}));
|
|
|
|
const requestData: SewerageSupplyRateSaveRequest = {
|
|
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
|
ctpvNm: searchValues?.sido ?? '',
|
|
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
|
statsYr: searchValues?.year ?? '',
|
|
userId: userInfo.userUnqId,
|
|
submitList: convertedData,
|
|
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt,
|
|
laePicNm: submitInfo.laePicNm,
|
|
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
|
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
|
laePicTelNo: submitInfo.laePicTelNo,
|
|
laeIdfrNm: submitInfo.laeIdfrNm,
|
|
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
|
|
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
|
|
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
|
|
};
|
|
|
|
setGlobalProgressLabel('일반 검증 진행 중...');
|
|
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
|
|
if (response.status === 'success') {
|
|
setGlobalProgress(50);
|
|
|
|
if (response.data.finalStatus === '5') {
|
|
setGlobalProgressLabel('IMR 검증 진행 중...');
|
|
const imrParam = {
|
|
user_id: userInfo.userUnqId,
|
|
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
|
stats_cd: statsCd,
|
|
stats_yr: searchValues?.year ?? '',
|
|
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
|
|
gen_verify_stts_cd: response.data.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 fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
|
await openVerificationResultDialogIfNeeded(
|
|
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
|
? {
|
|
sewerBzmnCd: searchValues.bsnmCd,
|
|
ctpvNm: searchValues.sido,
|
|
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
|
statsYr: searchValues.year,
|
|
statsCd,
|
|
}
|
|
: null,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert },
|
|
);
|
|
} catch (e) {
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
|
setIsLoading(false);
|
|
await openVerificationResultDialogIfNeeded(
|
|
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
|
? {
|
|
sewerBzmnCd: searchValues.bsnmCd,
|
|
ctpvNm: searchValues.sido,
|
|
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
|
statsYr: searchValues.year,
|
|
statsCd,
|
|
}
|
|
: null,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert, notifySubmitComplete: false },
|
|
);
|
|
const msg =
|
|
e instanceof Error && e.message
|
|
? e.message
|
|
: 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.';
|
|
alert(msg);
|
|
return;
|
|
}
|
|
} else {
|
|
setGlobalProgress(100);
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
|
await openVerificationResultDialogIfNeeded(
|
|
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
|
? {
|
|
sewerBzmnCd: searchValues.bsnmCd,
|
|
ctpvNm: searchValues.sido,
|
|
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
|
statsYr: searchValues.year,
|
|
statsCd,
|
|
}
|
|
: null,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert },
|
|
);
|
|
}
|
|
} else {
|
|
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
|
|
}
|
|
} catch {
|
|
alert('데이터 제출 중 오류가 발생했습니다.');
|
|
} finally {
|
|
hideGlobalProgress();
|
|
}
|
|
};
|
|
|
|
const saveFunc = async () => {
|
|
try {
|
|
if (await confirm('저장 하시겠습니까?')) {
|
|
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
|
if (verificationBlocked) {
|
|
alert(verificationBlocked);
|
|
return;
|
|
}
|
|
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
|
|
|
|
const userInfo = await getUserInfo();
|
|
const currentData = getGridDataRef.current?.();
|
|
|
|
if (!currentData || currentData.length === 0) {
|
|
alert('저장할 데이터가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 컬럼 정의에서 dataFormat 추출 (소수점 자리수 맞추기 위해)
|
|
const columnFormatMap = new Map<string, string>();
|
|
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
|
|
if (col.fieldName && col.dataFormat) {
|
|
columnFormatMap.set(col.fieldName, col.dataFormat);
|
|
}
|
|
});
|
|
|
|
// 숫자 포맷팅 함수 (dataFormat에 따라 소수점 자리수 맞춤)
|
|
const formatNumber = (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;
|
|
|
|
// #,##0.000 → 소수점 3자리, #,##0.0 → 소수점 1자리, #,##0 → 정수
|
|
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
|
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
|
};
|
|
|
|
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
|
// 행정구역
|
|
sggNm: normalizeGridString(d.sggNm), // 시군구
|
|
admdstCd: normalizeGridString(d.admdstCd), // 행정코드
|
|
admdstNm: normalizeGridString(d.admdstNm), // 행정구역명
|
|
trbtrNm: normalizeGridString(d.trbtrNm), // 지류
|
|
spcCtpnRgnCn:
|
|
d.spcCtpnRgnCn === '해당없음'
|
|
? null
|
|
: normalizeGridString(d.spcCtpnRgnCn), // 특별대책지역
|
|
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
|
|
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
|
|
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
|
|
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), // 고도처리
|
|
// 폐수처리인구(명) - 물리적/생물학적/고도처리
|
|
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
|
|
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
|
|
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
|
|
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), // 미접속인구
|
|
staaArea: formatNumber(d.staaArea, 'staaArea'), // 하수처리구역 내 면적(㎢)
|
|
// 하수처리구역 외
|
|
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), // 미처리인구
|
|
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
|
|
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
|
|
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
|
|
rmrk: normalizeGridString(d.rmrk), // 비고
|
|
// 합계·소계(계산필드 저장용)
|
|
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
|
|
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
|
|
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), // 하수처리구역 내 합계
|
|
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), // 공공하수처리인구 소계
|
|
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), // 폐수처리인구 소계
|
|
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), // 하수처리구역 외 합계
|
|
// 보급률(%)
|
|
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), // 공공하수처리구역 인구보급률
|
|
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), // 고도처리 인구보급률
|
|
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), // 하수도 설치율
|
|
}));
|
|
|
|
const requestData: SewerageSupplyRateSaveRequest = {
|
|
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드
|
|
ctpvNm: searchValues?.sido ?? '', // 시도
|
|
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
|
statsYr: searchValues?.year ?? '', // 통계 연도
|
|
userId: userInfo.userUnqId, // 사용자
|
|
saveList: convertedData, // 저장 데이터
|
|
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
|
|
};
|
|
|
|
// if (isAdminInfo) {
|
|
// requestData.superAdmin = 'superAdmin';
|
|
// }
|
|
|
|
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
|
|
|
|
if (response.status === 'success') {
|
|
setGlobalProgress(50);
|
|
setGlobalProgress(100);
|
|
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
|
alert('저장되었습니다.');
|
|
} else {
|
|
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
|
}
|
|
} else {
|
|
// 저장 취소
|
|
}
|
|
} catch {
|
|
alert('데이터 저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
hideGlobalProgress();
|
|
}
|
|
};
|
|
|
|
const cnacelSubmitFunc = async () => {
|
|
try {
|
|
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
|
|
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
|
if (verificationBlocked) {
|
|
alert(verificationBlocked);
|
|
return;
|
|
}
|
|
|
|
const userInfo = await getUserInfo();
|
|
|
|
const requestData: cancelSubmitRequest = {
|
|
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
|
ctpvNm: searchValues?.sido ?? '',
|
|
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
|
|
statsYr: searchValues?.year ?? '',
|
|
mdfrId: userInfo.userUnqId,
|
|
statsCd: statsCd,
|
|
};
|
|
|
|
const response = await statisticsApi.cancelSubmit(requestData);
|
|
|
|
if (response.status === 'success') {
|
|
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
|
alert('제출 취소되었습니다.');
|
|
} else {
|
|
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
} catch {
|
|
alert('제출 취소 중 오류가 발생했습니다.');
|
|
}
|
|
};
|
|
|
|
const openVerificationHistoryDialog = () => {
|
|
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !statsCd) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
setVerificationHistoryOpen(true);
|
|
};
|
|
|
|
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
|
|
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
|
: true;
|
|
|
|
const inputPeriodCurrent = isInputPeriodCurrent(periodCheckList);
|
|
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
|
|
const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
|
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
|
const showSubmittedInInputPeriodToolbar =
|
|
isSubmitted && inputPeriodCurrent && gugunOkOrNotDoList;
|
|
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
|
const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
|
const showActionToolbarCard =
|
|
rowData.length > 0 &&
|
|
toolbarGroupAllowed &&
|
|
(baseShowSaveInToolbar ||
|
|
baseShowEnterableToolbarButtons ||
|
|
showSubmittedInInputPeriodToolbar ||
|
|
(isSubmitted && isAdminInfo));
|
|
|
|
return (
|
|
<>
|
|
<div className="relative">
|
|
<SearchForm
|
|
key={searchFormRemountKey}
|
|
defaultValues={verificationSearchDefaults ?? undefined}
|
|
onSubmit={handleSearch}
|
|
onReset={handleReset}
|
|
onValuesChange={setSearchValues}
|
|
/>
|
|
{showActionToolbarCard && (
|
|
<div className="card !py-0">
|
|
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
|
<VerificationResultButton
|
|
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
|
/>
|
|
{showEnterableToolbarButtons && (
|
|
<>
|
|
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
|
<SubmitButton submitFunc={() => submitFunc(false)} />
|
|
</>
|
|
)}
|
|
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
|
|
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
|
<div className="relative size-[5rem]">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>}
|
|
<DataGrid
|
|
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
|
columnInfo={columnInfo}
|
|
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
|
|
rowStartNum={3}
|
|
fixedColumns={1}
|
|
rowData={rowData}
|
|
setRowData={setRowData}
|
|
statusInfo={statusInfo}
|
|
getGridDataRef={getGridDataRef}
|
|
cellStyleCallback={({ row, fieldName }) => {
|
|
const verification = (row as GridRowWithVerification).verificationResult;
|
|
if (!verification) return;
|
|
const errors = verification[fieldName];
|
|
if (Array.isArray(errors) && errors.length > 0) {
|
|
// IMR 검증은 노란색으로 구분
|
|
const hasImr = errors.some((e) => e.vrfcCd === 'IMR' || e.vrfcCd === 'HB');
|
|
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
|
|
}
|
|
}}
|
|
cellTooltipCallback={({ row, fieldName }) => {
|
|
const verification = (row as GridRowWithVerification).verificationResult;
|
|
if (!verification) return;
|
|
const errors = verification[fieldName];
|
|
if (Array.isArray(errors) && errors.length > 0) {
|
|
return errors
|
|
.map((e) => {
|
|
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NM' ? '일반검증 - ' : 'HB검증 -');
|
|
return `${prefix}${e.rsnExpln}
|
|
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<ConfirmDialog isOpen={false} />
|
|
<SubmitDailog
|
|
open={showDailog}
|
|
onOpenChange={(open) => {
|
|
setShowDailog(open);
|
|
if (!open) setIsExceptionSubmitFlow(false);
|
|
}}
|
|
statusInfo={statusInfo}
|
|
onConfirm={executeSubmit}
|
|
onExceptionConfirm={executeExceptionSubmit}
|
|
submitMode={isExceptionSubmitFlow ? 'exception' : 'normal'}
|
|
isLoading={isLoading}
|
|
/>
|
|
<VerificationResultHistoryDialog
|
|
open={verificationHistoryOpen}
|
|
onOpenChange={setVerificationHistoryOpen}
|
|
queryContext={
|
|
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
|
? {
|
|
sewerBzmnCd: searchValues.bsnmCd,
|
|
ctpvNm: searchValues.sido,
|
|
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
|
statsYr: searchValues.year,
|
|
statsCd,
|
|
}
|
|
: null
|
|
}
|
|
/>
|
|
<VerificationResultDialog
|
|
open={verificationResultDialogOpen}
|
|
onOpenChange={setVerificationResultDialogOpen}
|
|
queryContext={
|
|
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
|
? {
|
|
sewerBzmnCd: searchValues.bsnmCd,
|
|
ctpvNm: searchValues.sido,
|
|
sggNm:
|
|
searchValues.gugun === "ALL"
|
|
? ""
|
|
: (searchValues.gugun ?? ""),
|
|
statsYr: searchValues.year,
|
|
statsCd,
|
|
}
|
|
: null
|
|
}
|
|
onExceptionSubmitted={async () => {
|
|
const values = lastSearchedValues ?? searchValues;
|
|
if (!values) return;
|
|
await handleSearch(values);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SeweragePenetrationRateInp; |