[st] - 입력단 메뉴 작업중
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -210,7 +210,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
@@ -291,15 +291,15 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && submitList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
|
||||
const requestData = {
|
||||
@@ -530,19 +530,19 @@ const PublicSewageFacilityInp = () => {
|
||||
if (!(await confirm('저장 하시겠습니까?'))) return;
|
||||
|
||||
if (activeTab === 'ST00060001') {
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && saveList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const param: PublicSewageFacilitySaveRequest = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
ctpvNm: lastSearchedValues.sido ?? '',
|
||||
@@ -1309,6 +1309,11 @@ const PublicSewageFacilityInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
if (!ctx || !searchParamRef.current) return;
|
||||
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showDailog}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2119,6 +2119,12 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showDailog}
|
||||
|
||||
@@ -80,6 +80,12 @@ const SewagePipeMaintainInpTabList = [
|
||||
|
||||
const firstTabKey = RepairStatusInpColumn.tabKey;
|
||||
|
||||
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(
|
||||
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
@@ -302,6 +308,9 @@ const SewagePipeMaintainInp = () => {
|
||||
const [rowDataMap, setRowDataMap] = useState<
|
||||
Map<string, Record<string, string | number>[]>
|
||||
>(new Map());
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
>({});
|
||||
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
@@ -328,6 +337,37 @@ const SewagePipeMaintainInp = () => {
|
||||
}
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
@@ -951,22 +991,22 @@ const SewagePipeMaintainInp = () => {
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const columns = (tab.columns ?? []).map((col: ColumnConfig) =>
|
||||
canEdit
|
||||
? col
|
||||
: {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
},
|
||||
);
|
||||
const rawColumns = tab.columns ?? [];
|
||||
const base = canEdit
|
||||
? rawColumns
|
||||
: rawColumns.map((col) => ({ ...col, editable: false as const }));
|
||||
const columns = base
|
||||
.map((col) => {
|
||||
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
||||
return guide ? { ...col, tooltip: guide } : col;
|
||||
});
|
||||
map.set(tabKey, {
|
||||
columns,
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo]);
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
|
||||
|
||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
@@ -1120,7 +1160,7 @@ const SewagePipeMaintainInp = () => {
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={tabKey}
|
||||
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
|
||||
columnInfo={
|
||||
columnInfoByTab.get(tabKey) ??
|
||||
toGridColumnInfo(info)
|
||||
@@ -1163,6 +1203,17 @@ const SewagePipeMaintainInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
<SubmitDailog
|
||||
@@ -1176,6 +1227,17 @@ const SewagePipeMaintainInp = () => {
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -25,8 +26,11 @@ import SaveButton from "@/components/button/SaveButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
import DataGrid, { applyDropDownColumnToGrid } from "@/components/module/DataGrid";
|
||||
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||
import SubmitDailog from "./SubmitDailog";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import type {
|
||||
CellStyleCallbackParams,
|
||||
@@ -45,22 +49,27 @@ import {
|
||||
toCompactYmd,
|
||||
} from "@/components/util/grid/ymdFormat";
|
||||
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-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 {
|
||||
SEWAGE_RESERVOIR_STATS_CD,
|
||||
type SewageReservoirItem,
|
||||
type SewageReservoirRequestParams,
|
||||
type SewageReservoirSaveItem,
|
||||
type RainfallPumpStationItem,
|
||||
type RainfallPumpStationSaveItem,
|
||||
type SewageReservoirVerificationResult,
|
||||
} from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface";
|
||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import type {
|
||||
ApiResponse,
|
||||
cancelSubmitRequest,
|
||||
ImrStatusResponse,
|
||||
Sgg,
|
||||
SewerageStatusInfo,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
@@ -80,6 +89,12 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR
|
||||
const rainfallPumpTabKey =
|
||||
RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp";
|
||||
|
||||
const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(
|
||||
SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
};
|
||||
@@ -210,11 +225,7 @@ function buildSggNmColumn(
|
||||
sggOptions: Sgg[],
|
||||
): ColumnConfig {
|
||||
if (!canEdit) {
|
||||
return {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
};
|
||||
return { ...col, editable: false as const };
|
||||
}
|
||||
if (sggOptions.length === 0) {
|
||||
return { ...col, editable: true };
|
||||
@@ -272,31 +283,7 @@ function toGridNumber(value: string | number | null | undefined): number {
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
/** API 조회 항목 → 저장/삭제 API 형식 (조회 직후 스냅샷용) */
|
||||
function buildSewageReservoirSaveItemFromApiItem(
|
||||
item: SewageReservoirItem,
|
||||
): SewageReservoirSaveItem {
|
||||
return buildSewageReservoirSaveItem(
|
||||
item as unknown as Record<string, string | number>,
|
||||
);
|
||||
}
|
||||
|
||||
function buildSewageReservoirDeleteList(
|
||||
loadedSnapshot: SewageReservoirSaveItem[],
|
||||
currentRows: Record<string, string | number>[],
|
||||
): SewageReservoirSaveItem[] {
|
||||
const currentFcltSnSet = new Set(
|
||||
currentRows
|
||||
.map((row) => String(row.fcltSn ?? "").trim())
|
||||
.filter((sn) => sn !== ""),
|
||||
);
|
||||
return loadedSnapshot.filter((item) => {
|
||||
const sn = String(item.fcltSn ?? "").trim();
|
||||
return sn !== "" && !currentFcltSnSet.has(sn);
|
||||
});
|
||||
}
|
||||
|
||||
/** 시군구명(`sggNm`) 기준 행정구역 코드·명 */
|
||||
/** 시군구명(`sggNm`) 기준 `admdstCd`(bsnmCd)·`admdstNm`(시군구명) */
|
||||
function resolveSewageReservoirAdmdst(
|
||||
sggNm: string,
|
||||
sggOptions: Sgg[],
|
||||
@@ -304,7 +291,7 @@ function resolveSewageReservoirAdmdst(
|
||||
const trimmed = sggNm.trim();
|
||||
const matched = sggOptions.find((s) => s.gugun === trimmed);
|
||||
if (matched) {
|
||||
return { admdstCd: matched.adstDstrCd, admdstNm: matched.gugun };
|
||||
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
|
||||
}
|
||||
return { admdstCd: "", admdstNm: trimmed };
|
||||
}
|
||||
@@ -318,10 +305,12 @@ function buildSewageReservoirSaveItem(
|
||||
String(row.sggNm ?? ""),
|
||||
sggOptions,
|
||||
);
|
||||
const fcltSn = String(row.fcltSn ?? "").trim();
|
||||
return {
|
||||
admdstCd,
|
||||
admdstNm,
|
||||
fcltSn: String(row.fcltSn ?? ""),
|
||||
// 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1"
|
||||
fcltSn: fcltSn,
|
||||
fcltNm: String(row.fcltNm ?? ""),
|
||||
fcltPstnCn: String(row.fcltPstnCn ?? ""),
|
||||
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
|
||||
@@ -345,9 +334,9 @@ function buildSewageReservoirSaveItem(
|
||||
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
|
||||
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
|
||||
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
|
||||
prcsgFcltYn: String(row.prcsgFcltYn ?? ""),
|
||||
ddrztnEqpmYn: String(row.ddrztnEqpmYn ?? ""),
|
||||
oslfEtgtYn: String(row.oslfEtgtYn ?? ""),
|
||||
prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""),
|
||||
ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""),
|
||||
oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""),
|
||||
mltvMthCn: nullableTrimmedString(row.mltvMthCn),
|
||||
impdMthCn: nullableTrimmedString(row.impdMthCn),
|
||||
impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn),
|
||||
@@ -362,6 +351,56 @@ function buildSewageReservoirSaveItem(
|
||||
};
|
||||
}
|
||||
|
||||
/** 그리드 행 → ST00040002 저장/삭제 API 항목 */
|
||||
function buildRainfallPumpStationSaveItem(
|
||||
row: Record<string, string | number>,
|
||||
sggOptions: Sgg[] = [],
|
||||
): RainfallPumpStationSaveItem {
|
||||
const { admdstCd, admdstNm } = resolveSewageReservoirAdmdst(
|
||||
String(row.sggNm ?? ""),
|
||||
sggOptions,
|
||||
);
|
||||
const fcltSn = String(row.fcltSn ?? "").trim();
|
||||
return {
|
||||
admdstCd,
|
||||
admdstNm,
|
||||
fcltSn: fcltSn,
|
||||
fcltNm: String(row.fcltNm ?? ""),
|
||||
fcltPstnCn: String(row.fcltPstnCn ?? ""),
|
||||
fcltyKndCn: String(row.fcltyKndCn ?? ""),
|
||||
oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd),
|
||||
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
|
||||
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
|
||||
rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""),
|
||||
rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt),
|
||||
abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt),
|
||||
grssPpqty: toGridNumber(row.grssPpqty),
|
||||
pumpFcltN1DmtrExpln: String(row.pumpFcltN1DmtrExpln ?? ""),
|
||||
pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty),
|
||||
pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt),
|
||||
pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt),
|
||||
pumpFcltN2DmtrExpln: String(row.pumpFcltN2DmtrExpln ?? ""),
|
||||
pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty),
|
||||
pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt),
|
||||
pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt),
|
||||
pumpFcltN3DmtrExpln: String(row.pumpFcltN3DmtrExpln ?? ""),
|
||||
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
|
||||
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
|
||||
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
|
||||
prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""),
|
||||
ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""),
|
||||
oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""),
|
||||
dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""),
|
||||
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
|
||||
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
|
||||
prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""),
|
||||
ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""),
|
||||
oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
|
||||
grssMltvQnt: toGridNumber(row.grssMltvQnt),
|
||||
rmrk: nullableTrimmedString(row.rmrk),
|
||||
};
|
||||
}
|
||||
|
||||
function mapRainfallPumpStationListToGridRows(
|
||||
list: RainfallPumpStationItem[],
|
||||
ctpvNm: string,
|
||||
@@ -386,20 +425,72 @@ function mapRainfallPumpStationListToGridRows(
|
||||
});
|
||||
}
|
||||
|
||||
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 || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장
|
||||
const SewageReservoirPumpInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
||||
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
|
||||
const submitFunc = () => {
|
||||
console.log("submit");
|
||||
void (async () => {
|
||||
const tabKey = activeTabRef.current;
|
||||
if (
|
||||
tabKey !== SEWAGE_RESERVOIR_STATS_CD &&
|
||||
tabKey !== rainfallPumpTabKey
|
||||
) {
|
||||
alert("제출할 수 없는 탭입니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
||||
alert("시군구를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getCurrentRowsForTab(tabKey);
|
||||
if (!noApplicable && currentData.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
if (await confirm("제출 하시겠습니까?")) {
|
||||
setShowSubmitDialog(true);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
/** 마지막 조회 시 서버에 있던 하수저류시설 목록 (deleteList 비교 기준) */
|
||||
const loadedSewageReservoirSnapshotRef = useRef<SewageReservoirSaveItem[]>([]);
|
||||
|
||||
const cnacelSubmitFunc = () => {
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -530,24 +621,35 @@ const SewageReservoirPumpInp = () => {
|
||||
.filter((idx) => idx >= 0),
|
||||
);
|
||||
|
||||
const removedRows = currentRows.filter((_, idx) =>
|
||||
checkedIndexSet.has(idx),
|
||||
);
|
||||
const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx));
|
||||
if (nextRows.length === currentRows.length) {
|
||||
if (removedRows.length === 0) {
|
||||
alert("삭제할 항목을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
|
||||
setSewageReservoirDeleteList((prev) => [...prev, ...removedRows]);
|
||||
} else if (tabKey === rainfallPumpTabKey) {
|
||||
setRainfallPumpStationDeleteList((prev) => [...prev, ...removedRows]);
|
||||
}
|
||||
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, nextRows);
|
||||
return next;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
void (async () => {
|
||||
const tabKey = activeTabRef.current;
|
||||
if (tabKey !== SEWAGE_RESERVOIR_STATS_CD) {
|
||||
if (
|
||||
tabKey !== SEWAGE_RESERVOIR_STATS_CD &&
|
||||
tabKey !== rainfallPumpTabKey
|
||||
) {
|
||||
alert("저장할 수 없는 탭입니다.");
|
||||
return;
|
||||
}
|
||||
@@ -564,41 +666,63 @@ const SewageReservoirPumpInp = () => {
|
||||
commitGridForTab(tabKey);
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getCurrentRowsForTab(tabKey);
|
||||
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
const saveList = noApplicable
|
||||
? []
|
||||
: currentData.map((row) =>
|
||||
buildSewageReservoirSaveItem(row, sggOptions),
|
||||
);
|
||||
const deleteList = buildSewageReservoirDeleteList(
|
||||
loadedSewageReservoirSnapshotRef.current,
|
||||
currentData,
|
||||
);
|
||||
|
||||
if (!noApplicable && saveList.length === 0 && deleteList.length === 0) {
|
||||
alert("저장할 변경 사항이 없습니다.");
|
||||
: isReservoirTab
|
||||
? currentData.map((row) =>
|
||||
buildSewageReservoirSaveItem(row, sggOptions),
|
||||
)
|
||||
: currentData.map((row) =>
|
||||
buildRainfallPumpStationSaveItem(row, sggOptions),
|
||||
);
|
||||
const deleteList = isReservoirTab
|
||||
? sewageReservoirDeleteList
|
||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||
.map((row) => buildSewageReservoirSaveItem(row, sggOptions))
|
||||
: rainfallPumpStationDeleteList
|
||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||
.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions));
|
||||
if (!noApplicable && saveList.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestBase = {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
ctpvNm: lastSearchedValues.sido ?? "",
|
||||
sggNm:
|
||||
lastSearchedValues.gugun === "ALL"
|
||||
? ""
|
||||
: (lastSearchedValues.gugun ?? ""),
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
...(noApplicable ? { noApplicable: true } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
const response =
|
||||
await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
ctpvNm: lastSearchedValues.sido ?? "",
|
||||
sggNm:
|
||||
lastSearchedValues.gugun === "ALL"
|
||||
? ""
|
||||
: (lastSearchedValues.gugun ?? ""),
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
||||
const response = isReservoirTab
|
||||
? await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({
|
||||
...requestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
|
||||
...(noApplicable ? { noApplicable: true } : {}),
|
||||
saveList,
|
||||
deleteList,
|
||||
saveList: saveList as SewageReservoirSaveItem[],
|
||||
deleteList: deleteList as SewageReservoirSaveItem[],
|
||||
})
|
||||
: await statisticsInputSewageReservoirPumpApi.saveRainfallPumpStation({
|
||||
...requestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
saveList: saveList as RainfallPumpStationSaveItem[],
|
||||
deleteList: deleteList as RainfallPumpStationSaveItem[],
|
||||
});
|
||||
|
||||
if (response.status === "success") {
|
||||
if (isReservoirTab) {
|
||||
setSewageReservoirDeleteList([]);
|
||||
} else {
|
||||
setRainfallPumpStationDeleteList([]);
|
||||
}
|
||||
await loadActiveTabData(tabKey, lastSearchedValues, false, false);
|
||||
alert("저장되었습니다.");
|
||||
} else {
|
||||
@@ -612,6 +736,180 @@ const SewageReservoirPumpInp = () => {
|
||||
})();
|
||||
};
|
||||
|
||||
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const tabKey = activeTabRef.current;
|
||||
if (
|
||||
tabKey !== SEWAGE_RESERVOIR_STATS_CD &&
|
||||
tabKey !== rainfallPumpTabKey
|
||||
) {
|
||||
alert("제출할 수 없는 탭입니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
||||
alert("시군구를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
commitGridForTab(tabKey);
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getCurrentRowsForTab(tabKey);
|
||||
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
const submitList = noApplicable
|
||||
? []
|
||||
: isReservoirTab
|
||||
? currentData.map((row) => buildSewageReservoirSaveItem(row, sggOptions))
|
||||
: currentData.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions));
|
||||
const deleteList = isReservoirTab
|
||||
? sewageReservoirDeleteList
|
||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||
.map((row) => buildSewageReservoirSaveItem(row, sggOptions))
|
||||
: rainfallPumpStationDeleteList
|
||||
.filter((row) => String(row.fcltSn ?? "").trim() !== "")
|
||||
.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions));
|
||||
|
||||
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
||||
const verificationQuery = {
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
||||
ctpvNm: lastSearchedValues.sido ?? "",
|
||||
sggNm:
|
||||
lastSearchedValues.gugun === "ALL"
|
||||
? ""
|
||||
: (lastSearchedValues.gugun ?? ""),
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
statsCd: statsCdForTab,
|
||||
};
|
||||
|
||||
const reloadSubmittedTab = async () => {
|
||||
if (isReservoirTab) {
|
||||
setSewageReservoirDeleteList([]);
|
||||
} else {
|
||||
setRainfallPumpStationDeleteList([]);
|
||||
}
|
||||
await loadActiveTabData(tabKey, lastSearchedValues, false, false);
|
||||
};
|
||||
|
||||
if (!noApplicable && submitList.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitRequestBase = {
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
ctpvNm: lastSearchedValues.sido ?? "",
|
||||
sggNm:
|
||||
lastSearchedValues.gugun === "ALL"
|
||||
? ""
|
||||
: (lastSearchedValues.gugun ?? ""),
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
||||
...(noApplicable ? { noApplicable: true } : {}),
|
||||
laePicNm: submitInfo.laePicNm,
|
||||
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
||||
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
||||
laePicTelNo: submitInfo.laePicTelNo,
|
||||
laeIdfrNm: submitInfo.laeIdfrNm,
|
||||
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
|
||||
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
|
||||
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
|
||||
};
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: "서버에 요청 중...", progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
setGlobalProgressLabel("일반 검증 진행 중...");
|
||||
const response = isReservoirTab
|
||||
? await statisticsInputSewageReservoirPumpApi.submitSewageReservoir({
|
||||
...submitRequestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
submitList: submitList as SewageReservoirSaveItem[],
|
||||
deleteList: deleteList as SewageReservoirSaveItem[],
|
||||
})
|
||||
: await statisticsInputSewageReservoirPumpApi.submitRainfallPumpStation({
|
||||
...submitRequestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
submitList: submitList as RainfallPumpStationSaveItem[],
|
||||
deleteList: deleteList as RainfallPumpStationSaveItem[],
|
||||
});
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "데이터 제출 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const finalStatus = response.data?.finalStatus;
|
||||
const nvpassed = response.data?.nvpassed;
|
||||
|
||||
if (finalStatus === "5") {
|
||||
setGlobalProgress(50);
|
||||
setGlobalProgressLabel("IMR 검증 진행 중...");
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
|
||||
stats_cd: statsCdForTab,
|
||||
stats_yr: lastSearchedValues.year ?? "",
|
||||
lae_sbmsn_stts_cd:
|
||||
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
|
||||
gen_verify_stts_cd: nvpassed ? "1" : "2",
|
||||
};
|
||||
|
||||
try {
|
||||
const imrResponse = await statisticsApi.callImrService(imrParam);
|
||||
if (imrResponse.job_id) {
|
||||
await waitForImrJobCompleted(
|
||||
imrResponse.job_id,
|
||||
(id) => statisticsApi.getImrStatus(id),
|
||||
{
|
||||
onProgress: (jobProgress) => {
|
||||
setGlobalProgress(50 + Math.round(jobProgress / 2));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
setGlobalProgress(100);
|
||||
await reloadSubmittedTab();
|
||||
await openVerificationResultDialogIfNeeded(
|
||||
verificationQuery,
|
||||
setVerificationResultDialogOpen,
|
||||
{ notify: alert },
|
||||
);
|
||||
} catch (e) {
|
||||
await reloadSubmittedTab();
|
||||
await openVerificationResultDialogIfNeeded(
|
||||
verificationQuery,
|
||||
setVerificationResultDialogOpen,
|
||||
{ notify: alert, notifySubmitComplete: false },
|
||||
);
|
||||
const msg =
|
||||
e instanceof Error && e.message
|
||||
? e.message
|
||||
: "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.";
|
||||
alert(msg);
|
||||
}
|
||||
} else {
|
||||
setGlobalProgress(100);
|
||||
await reloadSubmittedTab();
|
||||
await openVerificationResultDialogIfNeeded(
|
||||
verificationQuery,
|
||||
setVerificationResultDialogOpen,
|
||||
{ notify: alert },
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 제출 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const [sewageReservoirDeleteList, setSewageReservoirDeleteList] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
const [rainfallPumpStationDeleteList, setRainfallPumpStationDeleteList] =
|
||||
useState<Record<string, string | number>[]>([]);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const [rowDataMap, setRowDataMap] = useState<
|
||||
Map<string, Record<string, string | number>[]>
|
||||
@@ -634,7 +932,13 @@ const SewageReservoirPumpInp = () => {
|
||||
>(new Map());
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
|
||||
useState(false);
|
||||
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
|
||||
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
>({});
|
||||
const reservoirGridRef = useRef<RealGridReact | null>(null);
|
||||
const rainfallPumpGridRef = useRef<RealGridReact | null>(null);
|
||||
const reservoirGridGetRowsRef =
|
||||
@@ -643,6 +947,36 @@ const SewageReservoirPumpInp = () => {
|
||||
useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
const activeTabRef = useRef(activeTab);
|
||||
@@ -732,9 +1066,7 @@ const SewageReservoirPumpInp = () => {
|
||||
|
||||
const { commonInfo, statusInfo, sewageReservoirList } = response.data;
|
||||
const list = sewageReservoirList ?? [];
|
||||
loadedSewageReservoirSnapshotRef.current = list
|
||||
.filter((item) => String(item.fcltSn ?? "").trim() !== "")
|
||||
.map(buildSewageReservoirSaveItemFromApiItem);
|
||||
setSewageReservoirDeleteList([]);
|
||||
const gridRows = mapSewageReservoirListToGridRows(
|
||||
list,
|
||||
commonInfo.ctpvNm,
|
||||
@@ -771,6 +1103,7 @@ const SewageReservoirPumpInp = () => {
|
||||
|
||||
const { commonInfo, statusInfo, rainfallPumpStationList } = response.data;
|
||||
const list = rainfallPumpStationList ?? [];
|
||||
setRainfallPumpStationDeleteList([]);
|
||||
const gridRows = mapRainfallPumpStationListToGridRows(
|
||||
list,
|
||||
commonInfo.ctpvNm,
|
||||
@@ -837,7 +1170,8 @@ const SewageReservoirPumpInp = () => {
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
loadedSewageReservoirSnapshotRef.current = [];
|
||||
setSewageReservoirDeleteList([]);
|
||||
setRainfallPumpStationDeleteList([]);
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
@@ -860,6 +1194,25 @@ const SewageReservoirPumpInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod(
|
||||
values.year || "",
|
||||
);
|
||||
if (periodResponse.status !== "success") {
|
||||
alert(periodResponse.message);
|
||||
return;
|
||||
}
|
||||
const periods = Array.isArray(periodResponse.data)
|
||||
? periodResponse.data
|
||||
: [];
|
||||
const periodOk = isInputPeriodCurrent(periods);
|
||||
periodEnterableRef.current = periodOk;
|
||||
setInputPeriodEnterable(periodOk);
|
||||
} catch {
|
||||
alert("입력 기간 확인 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const yearChangedFromLastSearch =
|
||||
lastSearchedValues != null &&
|
||||
(lastSearchedValues.year ?? "") !== (values.year ?? "");
|
||||
@@ -957,12 +1310,13 @@ const SewageReservoirPumpInp = () => {
|
||||
baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard =
|
||||
baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
// 항목추가/제거 버튼은 "데이터가 있든 없든", "과거 데이터(입력 불가 상태)여도"
|
||||
// 조회를 하고나면 항상 보여야 합니다.
|
||||
const isReservoirOrRainfallTab =
|
||||
activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey;
|
||||
const showArticleToolbarCard =
|
||||
hasSearched && isReservoirOrRainfallTab && toolbarGroupAllowed;
|
||||
hasSearched &&
|
||||
isReservoirOrRainfallTab &&
|
||||
toolbarGroupAllowed &&
|
||||
(showSaveInToolbar || showEnterableToolbarButtons);
|
||||
const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false;
|
||||
const showNotApplicableForActiveTab =
|
||||
loadedTabKeysRef.current.has(activeTab) &&
|
||||
@@ -1012,20 +1366,6 @@ const SewageReservoirPumpInp = () => {
|
||||
const sggNm =
|
||||
ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : "";
|
||||
|
||||
if (isRainfallPumpTab) {
|
||||
const newRow = createEmptyRainfallPumpStationRow(ctx.sido, sggNm);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const current = next.get(rainfallPumpTabKey) ?? [];
|
||||
next.set(rainfallPumpTabKey, [...current, newRow]);
|
||||
return next;
|
||||
});
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(rainfallPumpTabKey) ?? null;
|
||||
commitToolbarSnapshotForLoadedTab(rainfallPumpTabKey, statusInfo, true, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
let list = sggOptions;
|
||||
if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) {
|
||||
list = await loadSggOptions(ctx.sido);
|
||||
@@ -1036,24 +1376,23 @@ const SewageReservoirPumpInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabKey = SEWAGE_RESERVOIR_STATS_CD;
|
||||
const labels = list.map((s) => s.gugun).filter(Boolean);
|
||||
const newRow = createEmptySewageReservoirRow(ctx.sido, sggNm);
|
||||
const tabKey = activeTabKey;
|
||||
const newRow = isReservoirTab
|
||||
? createEmptySewageReservoirRow(ctx.sido, sggNm)
|
||||
: createEmptyRainfallPumpStationRow(ctx.sido, sggNm);
|
||||
|
||||
commitGridForTab(tabKey);
|
||||
const currentRows = getCurrentRowsForTab(tabKey);
|
||||
|
||||
flushSync(() => {
|
||||
setSggOptions(list);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const current = next.get(tabKey) ?? [];
|
||||
next.set(tabKey, [...current, newRow]);
|
||||
next.set(tabKey, [...currentRows, newRow]);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
// 드롭다운 컬럼은 columnInfoByTab(buildSggNmColumn) 렌더링으로 반영한다.
|
||||
// 행 추가 직후 setColumnProperty를 강제 재적용하면 타이밍 이슈로
|
||||
// `fieldIndex is out of bounds`가 발생할 수 있어 제거한다.
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx);
|
||||
};
|
||||
@@ -1068,24 +1407,29 @@ const SewageReservoirPumpInp = () => {
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
const columns = (tab.columns ?? []).map((col: ColumnConfig) => {
|
||||
if (isReservoirTab && col.fieldName === "ctpvNm") {
|
||||
return {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
};
|
||||
const isRainfallPumpTab = tabKey === rainfallPumpTabKey;
|
||||
const rawColumns = tab.columns ?? [];
|
||||
const base = canEdit
|
||||
? rawColumns
|
||||
: rawColumns.map((col) => ({ ...col, editable: false as const }));
|
||||
const columns = base.map((col: ColumnConfig) => {
|
||||
if (
|
||||
(isReservoirTab || isRainfallPumpTab) &&
|
||||
col.fieldName === "ctpvNm"
|
||||
) {
|
||||
return { ...col, editable: false as const };
|
||||
}
|
||||
if (isReservoirTab && col.fieldName === "sggNm") {
|
||||
return buildSggNmColumn(col, canEdit, sggOptions);
|
||||
if (
|
||||
(isReservoirTab || isRainfallPumpTab) &&
|
||||
col.fieldName === "sggNm" &&
|
||||
canEdit
|
||||
) {
|
||||
return buildSggNmColumn(col, true, sggOptions);
|
||||
}
|
||||
return canEdit
|
||||
? col
|
||||
: {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
};
|
||||
return col;
|
||||
}).map((col) => {
|
||||
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
||||
return guide ? { ...col, tooltip: guide } : col;
|
||||
});
|
||||
map.set(tabKey, {
|
||||
columns,
|
||||
@@ -1093,7 +1437,7 @@ const SewageReservoirPumpInp = () => {
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions]);
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByTab]);
|
||||
|
||||
const verificationCellStyle = ({
|
||||
fieldName,
|
||||
@@ -1220,7 +1564,7 @@ const SewageReservoirPumpInp = () => {
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={tabKey}
|
||||
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
|
||||
getGridRef={
|
||||
isReservoirTab
|
||||
? reservoirGridRef
|
||||
@@ -1265,7 +1609,7 @@ const SewageReservoirPumpInp = () => {
|
||||
showHeaderCheckbox
|
||||
statusInfo={tabStatusInfo}
|
||||
isExcelImport={
|
||||
tabRowDataExists(rowDataMap, tabKey) &&
|
||||
loadedTabKeysRef.current.has(tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOk
|
||||
}
|
||||
@@ -1281,6 +1625,28 @@ const SewageReservoirPumpInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values) return;
|
||||
await loadActiveTabData(activeTabRef.current, values, false, false);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showSubmitDialog}
|
||||
onOpenChange={setShowSubmitDialog}
|
||||
statusInfo={activeTabStatus}
|
||||
onConfirm={executeSubmit}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values) return;
|
||||
await loadActiveTabData(activeTabRef.current, values, false, false);
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
</>
|
||||
|
||||
@@ -1,34 +1,539 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/input/statistics-input-sewerage-finance-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
CauserBurdenItem,
|
||||
SewerageChargeItem,
|
||||
SewerageFinanceRequestParams,
|
||||
SewerageFinanceSettleItem,
|
||||
SewerageFinanceVerificationResult,
|
||||
SewerageUsageFeeItem,
|
||||
} from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface";
|
||||
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn";
|
||||
import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn";
|
||||
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn";
|
||||
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn"
|
||||
import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn"
|
||||
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn"
|
||||
import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn"
|
||||
const SewerageFinanceInpTabList = [
|
||||
SewerageFinanceSettleInpColumn,
|
||||
SewerageChargeInpColumn,
|
||||
SewerageUsageFeeInpColumn,
|
||||
CauserBurdenInpColumn,
|
||||
] as const;
|
||||
|
||||
const firstTabKey = SewerageFinanceSettleInpColumn.tabKey;
|
||||
|
||||
const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageFinanceVerificationResult;
|
||||
};
|
||||
|
||||
type TabToolbarSnapshot = {
|
||||
showSaveInToolbar: boolean;
|
||||
showEnterableToolbarButtons: boolean;
|
||||
showActionToolbarCard: boolean;
|
||||
};
|
||||
|
||||
const toGridColumnInfo = (tab: (typeof SewerageFinanceInpTabList)[number]): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
|
||||
function buildSewerageFinanceSearchParam(
|
||||
values: SearchFormValues,
|
||||
overrides?: { mode?: string },
|
||||
): SewerageFinanceRequestParams {
|
||||
return {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
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 tabRowDataExists(
|
||||
m: Map<string, Record<string, string | number>[]>,
|
||||
tabKey: string,
|
||||
): boolean {
|
||||
const rows = m.get(tabKey);
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
}
|
||||
|
||||
function mapRegionalListToGridRows<
|
||||
T extends { verificationResult?: SewerageFinanceVerificationResult },
|
||||
>(list: T[], ctpvNm: string): GridRowWithVerification[] {
|
||||
return list.map((item) => {
|
||||
const { verificationResult, ...rest } = item;
|
||||
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
|
||||
if (verificationResult) row.verificationResult = verificationResult;
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 09. 하수도재정
|
||||
const SewerageFinanceInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
// tab list
|
||||
const SewerageFinanceInpTabList = [
|
||||
SewerageFinanceSettleInpColumn,
|
||||
SewerageChargeInpColumn,
|
||||
SewerageUsageFeeInpColumn,
|
||||
CauserBurdenInpColumn
|
||||
];
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(
|
||||
new Map(),
|
||||
);
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
|
||||
Map<string, SewerageStatusInfo | null>
|
||||
>(new Map());
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
|
||||
Map<string, TabToolbarSnapshot>
|
||||
>(new Map());
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
>({});
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
||||
|
||||
const submitExceptionFunc = () => {
|
||||
// 예외 제출 로직
|
||||
const searchParamRef = useRef<SewerageFinanceRequestParams | null>(null);
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
const activeTabRef = useRef(activeTab);
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
hasRowData: boolean,
|
||||
searchContext: SearchFormValues,
|
||||
) => {
|
||||
setToolbarSnapshotByTab((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(
|
||||
tabKey,
|
||||
computeTabToolbarSnapshot(
|
||||
statusInfo,
|
||||
hasRowData,
|
||||
searchDoneRef.current,
|
||||
searchContext,
|
||||
periodEnterableRef.current,
|
||||
isAdminInfo,
|
||||
),
|
||||
);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const lastYearDataFunc = () => {
|
||||
// 지난연도 데이터 가져오기 로직
|
||||
const applyRowMap = (
|
||||
mergeIntoExistingMap: boolean,
|
||||
apply: (m: Map<string, Record<string, string | number>[]>) => void,
|
||||
) => {
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
apply(next);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
apply(next);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
};
|
||||
|
||||
const loadActiveTabData = async (
|
||||
tabKey: string,
|
||||
param: SewerageFinanceRequestParams,
|
||||
searchContext: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
updateLastSearchedValues: boolean,
|
||||
overrides?: { mode?: string },
|
||||
): Promise<boolean> => {
|
||||
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
|
||||
|
||||
if (tabKey === SewerageFinanceSettleInpColumn.tabKey) {
|
||||
const response =
|
||||
await statisticsInputSewerageFinanceApi.getSewerageFinanceSettleList(requestParam);
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
const { commonInfo, statusInfo, sewerageFinanceSettleList } = response.data;
|
||||
const gridRows = mapRegionalListToGridRows(
|
||||
(sewerageFinanceSettleList ?? []) as SewerageFinanceSettleItem[],
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(SewerageFinanceSettleInpColumn.tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabKey === SewerageChargeInpColumn.tabKey) {
|
||||
const response =
|
||||
await statisticsInputSewerageFinanceApi.getSewerageChargeList(requestParam);
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
const { commonInfo, statusInfo, sewerageChargeList } = response.data;
|
||||
const gridRows = mapRegionalListToGridRows(
|
||||
(sewerageChargeList ?? []) as SewerageChargeItem[],
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(SewerageChargeInpColumn.tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabKey === SewerageUsageFeeInpColumn.tabKey) {
|
||||
const response =
|
||||
await statisticsInputSewerageFinanceApi.getSewerageUsageFeeList(requestParam);
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
const { commonInfo, statusInfo, sewerageUsageFeeList } = response.data;
|
||||
const gridRows = mapRegionalListToGridRows(
|
||||
(sewerageUsageFeeList ?? []) as SewerageUsageFeeItem[],
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(SewerageUsageFeeInpColumn.tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabKey === CauserBurdenInpColumn.tabKey) {
|
||||
const response =
|
||||
await statisticsInputSewerageFinanceApi.getCauserBurdenList(requestParam);
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
const { commonInfo, statusInfo, causerBurdenList } = response.data;
|
||||
const gridRows = mapRegionalListToGridRows(
|
||||
(causerBurdenList ?? []) as CauserBurdenItem[],
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(CauserBurdenInpColumn.tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
alert("해당 탭 조회 API가 없습니다.");
|
||||
return false;
|
||||
};
|
||||
|
||||
const getTabGridRows = (tabKey: string): Record<string, string | number>[] =>
|
||||
getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
activeTabRef.current = firstTabKey;
|
||||
setLastSearchedValues(null);
|
||||
setHasSearched(false);
|
||||
setInputPeriodEnterable(true);
|
||||
searchParamRef.current = null;
|
||||
searchDoneRef.current = false;
|
||||
periodEnterableRef.current = 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 : [];
|
||||
const periodOk = isInputPeriodCurrent(periods);
|
||||
periodEnterableRef.current = periodOk;
|
||||
setInputPeriodEnterable(periodOk);
|
||||
|
||||
const yearChangedFromLastSearch =
|
||||
lastSearchedValues != null &&
|
||||
(lastSearchedValues.year ?? "") !== (values.year ?? "");
|
||||
|
||||
if (yearChangedFromLastSearch) {
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
searchDoneRef.current = true;
|
||||
setHasSearched(true);
|
||||
const searchParam = buildSewerageFinanceSearchParam(values);
|
||||
searchParamRef.current = searchParam;
|
||||
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParam,
|
||||
values,
|
||||
!yearChangedFromLastSearch,
|
||||
true,
|
||||
);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
const prevTabKey = activeTabRef.current;
|
||||
const latestRowsInPrevTab = getTabGridRows(prevTabKey);
|
||||
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(prevTabKey, latestRowsInPrevTab);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
|
||||
if (tabRowDataExists(rowDataMap, tabKey)) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadActiveTabData(
|
||||
tabKey,
|
||||
searchParamRef.current,
|
||||
lastSearchedValues,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const lastYearDataFunc = async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!hasSearched) {
|
||||
alert("먼저 데이터를 조회해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const param = buildSewerageFinanceSearchParam(values, { mode: "PRE" });
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
param,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
{ mode: "PRE" },
|
||||
);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cnacelSubmitFunc = () => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return;
|
||||
|
||||
if (!lastSearchedValues || !searchParamRef.current) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const tabKey = activeTabRef.current;
|
||||
const statsCdForTab = tabStatsCdMap.get(tabKey);
|
||||
if (!statsCdForTab) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
||||
ctpvNm: lastSearchedValues.sido ?? "",
|
||||
sggNm:
|
||||
lastSearchedValues.gugun === "ALL"
|
||||
? ""
|
||||
: (lastSearchedValues.gugun ?? ""),
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: statsCdForTab,
|
||||
};
|
||||
|
||||
const response = await statisticsApi.cancelSubmit(requestData);
|
||||
|
||||
if (response.status === "success") {
|
||||
setIsLoading(true);
|
||||
await loadActiveTabData(
|
||||
tabKey,
|
||||
searchParamRef.current,
|
||||
lastSearchedValues,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
alert("제출 취소되었습니다.");
|
||||
} else {
|
||||
alert(response.message ?? "제출 취소 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("제출 취소 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const submitFunc = () => {
|
||||
@@ -39,26 +544,249 @@ const SewerageFinanceInp = () => {
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const openVerificationHistoryDialog = () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
const statsCd = tabStatsCdMap.get(activeTab);
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
setVerificationHistoryOpen(true);
|
||||
};
|
||||
|
||||
const verificationQueryContext = (() => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
const statsCd = tabStatsCdMap.get(activeTab);
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
|
||||
return {
|
||||
sewerBzmnCd: ctx.bsnmCd,
|
||||
ctpvNm: ctx.sido,
|
||||
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
||||
statsYr: ctx.year,
|
||||
statsCd,
|
||||
};
|
||||
})();
|
||||
|
||||
const columnInfoByTab = useMemo(() => {
|
||||
const map = new Map<string, ColumnInfo>();
|
||||
for (const tab of SewerageFinanceInpTabList) {
|
||||
const tabKey = tab.tabKey;
|
||||
const canEdit =
|
||||
computeTabEnterable(
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
|
||||
const base = canEdit
|
||||
? rawColumns
|
||||
: rawColumns.map((col) => ({ ...col, editable: false as const }));
|
||||
const columns = base.map((col) => {
|
||||
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
||||
return guide ? { ...col, tooltip: guide } : col;
|
||||
});
|
||||
map.set(tabKey, {
|
||||
columns,
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
|
||||
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const activeTabToolbar =
|
||||
toolbarSnapshotByTab.get(activeTab) ??
|
||||
computeTabToolbarSnapshot(
|
||||
activeTabStatus,
|
||||
tabRowDataExists(rowDataMap, activeTab),
|
||||
hasSearched,
|
||||
lastSearchedValues,
|
||||
inputPeriodEnterable,
|
||||
isAdminInfo,
|
||||
);
|
||||
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } =
|
||||
activeTabToolbar;
|
||||
|
||||
const verificationCellStyle = ({
|
||||
fieldName,
|
||||
row,
|
||||
}: {
|
||||
fieldName: string;
|
||||
row: Record<string, string | number>;
|
||||
}): string | undefined => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
|
||||
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
||||
}
|
||||
};
|
||||
|
||||
const verificationCellTooltip = ({
|
||||
fieldName,
|
||||
row,
|
||||
}: {
|
||||
fieldName: string;
|
||||
row: Record<string, string | number>;
|
||||
}): string | undefined => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR"
|
||||
? "IMR검증 - "
|
||||
: e.vrfcCd === "NV"
|
||||
? "일반검증 - "
|
||||
: "HB검증 - ";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<SubmitExceptionButton SubmitExceptionFunc={submitExceptionFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={lastYearDataFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<div className="relative">
|
||||
<SearchForm
|
||||
onSubmit={handleSearch}
|
||||
onReset={handleReset}
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
<LastYearDataButton
|
||||
getLastYearDataFunc={() => void lastYearDataFunc()}
|
||||
/>
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
|
||||
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
||||
)}
|
||||
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<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>
|
||||
)}
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{SewerageFinanceInpTabList.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{SewerageFinanceInpTabList.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const tabEnterableForGrid =
|
||||
computeTabEnterable(
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
|
||||
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
|
||||
columnInfo={
|
||||
columnInfoByTab.get(tabKey) ?? toGridColumnInfo(info)
|
||||
}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function"
|
||||
? updater(cur)
|
||||
: updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
statusInfo={tabStatusInfo}
|
||||
isExcelImport={
|
||||
tabRowDataExists(rowDataMap, tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOk
|
||||
}
|
||||
isExcelExport
|
||||
cellStyleCallback={verificationCellStyle}
|
||||
cellTooltipCallback={verificationCellTooltip}
|
||||
getGridDataRef={
|
||||
activeTab === tabKey ? getGridDataRef : undefined
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<TabDataGrid
|
||||
columnInfo={SewerageFinanceInpTabList}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
<VerificationResultHistoryDialog
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SewerageFinanceInp;
|
||||
export default SewerageFinanceInp;
|
||||
|
||||
@@ -288,7 +288,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
@@ -790,11 +791,13 @@ const SeweragePenetrationRateInp = () => {
|
||||
</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" />
|
||||
{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>
|
||||
</div>}
|
||||
}
|
||||
<DataGrid
|
||||
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
@@ -859,6 +862,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values) return;
|
||||
await handleSearch(values);
|
||||
}}
|
||||
/>
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user