=> {
- // 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
- setIsNotApplicable(false);
try {
if (tabKey === 'ST00060001') {
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
@@ -677,8 +691,10 @@ const PublicSewageFacilityInp = () => {
if (list.length === 0) {
setShowIsNotApplicable(true);
+ setIsNotApplicable(true);
} else {
setShowIsNotApplicable(false);
+ setIsNotApplicable(false);
}
const gridRows = toGridRows(convertedData);
@@ -692,6 +708,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
+ setIsVerificationFailedByTab((prev) =>
+ patchVerificationFailedByTab(prev, tabKey, commonInfo),
+ );
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', gridRows);
@@ -736,6 +755,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
+ setIsVerificationFailedByTab((prev) =>
+ patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo),
+ );
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacility', leftGridRows);
@@ -753,6 +775,52 @@ const PublicSewageFacilityInp = () => {
}
};
+ const updateOperateDataButtonFunc = () => {
+ void (async () => {
+ if (!lastSearchedValues) {
+ alert('먼저 조회를 진행해주세요.');
+ return;
+ }
+ if (!(await confirm('운영 데이터를 업데이트 하시겠습니까?'))) return;
+
+ try {
+ showGlobalProgress({ label: '운영 데이터 업데이트 중...', progress: 0 });
+ const userInfo = await getUserInfo();
+ const request = {
+ statsYr: lastSearchedValues.year ?? '',
+ ctpvNm: lastSearchedValues.sido ?? '',
+ sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
+ sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
+ userId: userInfo.userUnqId,
+ };
+
+ const response =
+ await statisticsInputPublicSewageFacilityApiService.updatePublicSewageFacilityOperationData(
+ request,
+ );
+ if (response.status !== 'success') {
+ alert(response.message || '운영 데이터 업데이트 중 오류가 발생했습니다.');
+ return;
+ }
+
+ setGlobalProgress(50);
+ const param: PublicSewageFacilityRequestParams = {
+ statsYr: request.statsYr,
+ ctpvNm: request.ctpvNm,
+ sggNm: request.sggNm,
+ sewerBzmnCd: request.sewerBzmnCd,
+ };
+ await loadPublicSewageFacilityActiveTabData('ST00060001', param, lastSearchedValues);
+ setGlobalProgress(100);
+ alert('운영 데이터가 업데이트되었습니다.');
+ } catch {
+ alert('운영 데이터 업데이트 중 오류가 발생했습니다.');
+ } finally {
+ hideGlobalProgress();
+ }
+ })();
+ };
+
const runMainFacilityMappingSave = async (
draft: MainFacilityMappingSaveDraft,
dialogInput: MainFacilityMappingSaveDialogValues | null,
@@ -820,7 +888,10 @@ const PublicSewageFacilityInp = () => {
}
};
- const handleSearch = async (values: SearchFormValues) => {
+ const handleSearch = async (
+ values: SearchFormValues,
+ opts?: { forcedTabKey?: string },
+ ) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
@@ -848,6 +919,7 @@ const PublicSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
+ setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -860,7 +932,13 @@ const PublicSewageFacilityInp = () => {
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
searchParamRef.current = param;
- const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
+ const tabKeyToLoad =
+ opts?.forcedTabKey ??
+ (yearChangedFromLastSearch ? firstTabKey : activeTabRef.current);
+ if (opts?.forcedTabKey) {
+ activeTabRef.current = opts.forcedTabKey;
+ setActiveTab(opts.forcedTabKey);
+ }
await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
@@ -870,6 +948,19 @@ const PublicSewageFacilityInp = () => {
};
const handleActiveTabChange = async (tabKey: string) => {
+ const prevTabKey = activeTabRef.current;
+ if (prevTabKey === 'ST00060001') {
+ const latestRowsInPrevTab =
+ st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
+ if (latestRowsInPrevTab.length > 0 || rowDataMap.has('ST00060001')) {
+ setRowDataMap((prev) => {
+ const next = new Map(prev);
+ next.set('ST00060001', latestRowsInPrevTab);
+ return next;
+ });
+ }
+ }
+
activeTabRef.current = tabKey;
setActiveTab(tabKey);
@@ -885,12 +976,13 @@ const PublicSewageFacilityInp = () => {
const handleReset = () => {
setIsLoading(false);
setHasSearched(false);
- setIsNotApplicable(false);
+ setIsNotApplicableByTab(new Map());
setShowIsNotApplicable(false);
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
+ setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setInputPeriodEnterable(true);
@@ -1066,10 +1158,23 @@ const PublicSewageFacilityInp = () => {
};
})();
+ const { verificationSearchDefaults, searchFormRemountKey } =
+ useStatisticsInputPendingNavigation({
+ urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY,
+ onPending: (pending) => {
+ activeTabRef.current = pending.tabKey;
+ setActiveTab(pending.tabKey);
+ },
+ onAutoSearch: (values, tabKey) =>
+ handleSearch(values, { forcedTabKey: tabKey }),
+ });
+
return (
<>
{
{!isMainFacilityTab && (
)}
{showEnterableToolbarButtons && !isMainFacilityTab && (
@@ -1215,7 +1323,7 @@ const PublicSewageFacilityInp = () => {
return (
{
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
+ onExceptionResetSubmitted={async () => {
+ const ctx = lastSearchedValues ?? searchValues;
+ if (!ctx || !searchParamRef.current) return;
+ await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
+ }}
/>
05. 펌프장
+type GridRowWithVerification = Record & {
+ verificationResult?: PumpingStationVerificationResult;
+};
+
+type TabToolbarSnapshot = {
+ showSaveInToolbar: boolean;
+ showEnterableToolbarButtons: boolean;
+ showActionToolbarCard: boolean;
+};
+
+const computeTabEnterable = (
+ statusInfo: SewerageStatusInfo | null | undefined,
+ periodEnterable: boolean,
+): boolean => {
+ if (statusInfo?.laeSbmsnSttsCd) {
+ if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
+ return false;
+ }
+ return periodEnterable;
+ }
+ return periodEnterable;
+};
+
+function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
+ return doList.includes(values?.bsnmCd || "")
+ ? Boolean(values?.gugun && values.gugun !== "ALL")
+ : true;
+}
+
+function computeTabToolbarSnapshot(
+ statusInfo: SewerageStatusInfo | null,
+ hasRowData: boolean,
+ treatAsSearched: boolean,
+ searchContext: SearchFormValues | null,
+ periodEnterable: boolean,
+ isAdminInfo: boolean,
+): TabToolbarSnapshot {
+ const gugunOk = buildGugunOkForToolbar(searchContext);
+ const enterable = computeTabEnterable(statusInfo, periodEnterable);
+ const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
+ const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
+ const showEnterableToolbarButtons = enterable && gugunOk;
+ const showSubmittedInInputPeriodToolbar =
+ isSubmitted && periodEnterable && gugunOk;
+ const showActionToolbarCard =
+ (hasRowData || treatAsSearched) &&
+ (showSaveInToolbar ||
+ showEnterableToolbarButtons ||
+ showSubmittedInInputPeriodToolbar ||
+ (isSubmitted && isAdminInfo));
+ return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard };
+}
+
+function buildPumpingStationSearchParam(
+ values: SearchFormValues,
+ overrides?: { mode?: string },
+): PumpingStationRequestParams {
+ const fcltNm = values.fctName?.trim();
+ return {
+ statsYr: values.year,
+ sewerBzmnCd: values.bsnmCd,
+ ctpvNm: values.sido,
+ sggNm: values.gugun === "ALL" ? "" : values.gugun,
+ ...(fcltNm ? { fcltNm } : {}),
+ ...(overrides?.mode ? { mode: overrides.mode } : {}),
+ };
+}
+
+function parseSggListFromResponse(res: ApiResponse): Sgg[] {
+ if (res.status !== "success" || res.data == null) return [];
+ return Array.isArray(res.data) ? res.data : [];
+}
+
+function buildSggNmColumn(
+ col: ColumnConfig,
+ canEdit: boolean,
+ sggOptions: Sgg[],
+): ColumnConfig {
+ if (!canEdit) {
+ return { ...col, editable: false as const };
+ }
+ if (sggOptions.length === 0) {
+ return { ...col, editable: true };
+ }
+ const labels = sggOptions.map((s) => s.gugun);
+ return {
+ ...col,
+ editable: true,
+ editor: {
+ type: "dropdown",
+ domainOnly: true,
+ textReadOnly: true,
+ commitOnSelect: true,
+ dropDownWhenClick: true,
+ values: labels,
+ labels,
+ },
+ values: labels,
+ labels,
+ lookupDisplay: true,
+ };
+}
+
+function mapPumpingStationListToGridRows(
+ list: PumpingStationItem[],
+ ctpvNm: string,
+): GridRowWithVerification[] {
+ return list.map((item) => {
+ const row = toGridRows([
+ {
+ ...item,
+ ctpvNm,
+ oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
+ },
+ ])[0] as GridRowWithVerification;
+ if (item.verificationResult) {
+ row.verificationResult = item.verificationResult;
+ }
+ return row;
+ });
+}
+
+function nullableTrimmedString(
+ value: string | number | null | undefined,
+): string | null {
+ const s = String(value ?? "").trim();
+ return s === "" ? null : s;
+}
+
+function toGridNumber(value: string | number | null | undefined): number {
+ const n = Number(value);
+ return Number.isFinite(n) ? n : 0;
+}
+
+function resolvePumpingStationAdmdst(
+ row: Record,
+ sggOptions: Sgg[],
+): { admdstCd: string; admdstNm: string } {
+ const existingCd = String(row.admdstCd ?? "").trim();
+ const existingNm = String(row.admdstNm ?? "").trim();
+ if (existingCd && existingNm) {
+ return { admdstCd: existingCd, admdstNm: existingNm };
+ }
+ const sggNm = String(row.sggNm ?? "").trim();
+ const matched = sggOptions.find((s) => s.gugun === sggNm);
+ if (matched) {
+ return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
+ }
+ return { admdstCd: existingCd, admdstNm: existingNm || sggNm };
+}
+
+/** 그리드 행 → ST00050001 저장/삭제 API 항목 */
+function buildPumpingStationSaveItem(
+ row: Record,
+ sggOptions: Sgg[] = [],
+): PumpingStationSaveItem {
+ const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions);
+ const fcltSn = String(row.fcltSn ?? "").trim();
+ return {
+ admdstCd,
+ admdstNm,
+ fcltSn: fcltSn,
+ swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""),
+ fcltNm: String(row.fcltNm ?? ""),
+ pumpSeNm: String(row.pumpSeNm ?? ""),
+ fcltPstnCn: String(row.fcltPstnCn ?? ""),
+ oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
+ planMltvArea: toGridNumber(row.planMltvArea),
+ sndyArea: toGridNumber(row.sndyArea),
+ rainArea: toGridNumber(row.rainArea),
+ drtwPumpN1DmtrArea: toGridNumber(row.drtwPumpN1DmtrArea),
+ drtwPumpN1Ppqty: toGridNumber(row.drtwPumpN1Ppqty),
+ drtwPumpN1Cnt: toGridNumber(row.drtwPumpN1Cnt),
+ drtwPumpN1RsrvCnt: toGridNumber(row.drtwPumpN1RsrvCnt),
+ drtwPumpN2DmtrArea: toGridNumber(row.drtwPumpN2DmtrArea),
+ drtwPumpN2Ppqty: toGridNumber(row.drtwPumpN2Ppqty),
+ drtwPumpN2Cnt: toGridNumber(row.drtwPumpN2Cnt),
+ drtwPumpN2RsrvCnt: toGridNumber(row.drtwPumpN2RsrvCnt),
+ drtwPumpN3DmtrArea: toGridNumber(row.drtwPumpN3DmtrArea),
+ drtwPumpN3Ppqty: toGridNumber(row.drtwPumpN3Ppqty),
+ drtwPumpN3Cnt: toGridNumber(row.drtwPumpN3Cnt),
+ drtwPumpN3RsrvCnt: toGridNumber(row.drtwPumpN3RsrvCnt),
+ exlnPumpN1DmtrArea: toGridNumber(row.exlnPumpN1DmtrArea),
+ exlnPumpN1Ppqty: toGridNumber(row.exlnPumpN1Ppqty),
+ exlnPumpN1Cnt: toGridNumber(row.exlnPumpN1Cnt),
+ exlnPumpN1RsrvCnt: toGridNumber(row.exlnPumpN1RsrvCnt),
+ exlnPumpN2DmtrArea: toGridNumber(row.exlnPumpN2DmtrArea),
+ exlnPumpN2Ppqty: toGridNumber(row.exlnPumpN2Ppqty),
+ exlnPumpN2Cnt: toGridNumber(row.exlnPumpN2Cnt),
+ exlnPumpN2RsrvCnt: toGridNumber(row.exlnPumpN2RsrvCnt),
+ exlnPumpN3DmtrArea: toGridNumber(row.exlnPumpN3DmtrArea),
+ exlnPumpN3Ppqty: toGridNumber(row.exlnPumpN3Ppqty),
+ exlnPumpN3Cnt: toGridNumber(row.exlnPumpN3Cnt),
+ exlnPumpN3RsrvCnt: toGridNumber(row.exlnPumpN3RsrvCnt),
+ anlElpwrUseQnt: toGridNumber(row.anlElpwrUseQnt),
+ rmrk: nullableTrimmedString(row.rmrk),
+ };
+}
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+async function waitForImrJobCompleted(
+ jobId: string,
+ getStatus: (id: string) => Promise,
+ options?: {
+ pollIntervalMs?: number;
+ maxAttempts?: number;
+ onProgress?: (jobProgress: number) => void;
+ },
+): Promise {
+ const pollIntervalMs = options?.pollIntervalMs ?? 2000;
+ const maxAttempts = options?.maxAttempts ?? 150;
+ for (let i = 0; i < maxAttempts; i++) {
+ const res = await getStatus(jobId);
+ options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
+ if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
+ if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
+ throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
+ }
+ await sleep(pollIntervalMs);
+ }
+ throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
+}
+
+function createEmptyPumpingStationRow(
+ ctpvNm: string,
+ sggNm = "",
+): GridRowWithVerification {
+ const row: GridRowWithVerification = { ctpvNm, sggNm };
+ for (const col of PumpingStationInpColumn.columns) {
+ const fieldName = col.fieldName;
+ if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue;
+ row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : "";
+ }
+ return row;
+}
+
+// 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴)
const PumpingStationInp = () => {
-
- const SubmitExceptionFunc = () => {
- console.log('submit exception');
- // 예외 제출 로직
- };
-
- const getLastYearDataFunc = () => {
- console.log('get last year data');
- // 지난연도 데이터 가져오기 로직
- };
-
- const submitFunc = () => {
- console.log('submit');
- // 제출 로직
- };
-
- const saveFunc = () => {
- console.log('save');
- // 저장 로직
- };
-
- const addArticleFunc = () => {
- console.log('save');
- // 항목 추가 로직
- };
-
- const deleteArticleFunc = () => {
- console.log('save');
- // 항목 제외 로직
- };
+ 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 [rowData, setRowData] = useState[]>([]);
const [isNotApplicable, setIsNotApplicable] = useState(false);
+ const [searchValues, setSearchValues] = useState(null);
+ const [lastSearchedValues, setLastSearchedValues] =
+ useState(null);
+ const [hasSearched, setHasSearched] = useState(false);
+ const [statusInfo, setStatusInfo] = useState(null);
+ const [statsCd, setStatsCd] = useState("");
+ const [isVerificationFailed, setIsVerificationFailed] = useState(false);
+ const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
+ const [sggOptions, setSggOptions] = useState([]);
+ const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<
+ Record
+ >({});
+ const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
+ const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
+ useState(false);
+ const [showSubmitDialog, setShowSubmitDialog] = useState(false);
+ const [pumpingStationDeleteList, setPumpingStationDeleteList] = useState<
+ Record[]
+ >([]);
+
+ const gridRef = useRef(null);
+ const gridGetRowsRef =
+ useRef<(() => Record[]) | null>(null);
+ const sggOptionsSidoRef = useRef(null);
+ const [searchLoaded, setSearchLoaded] = useState(false);
+
+ useEffect(() => {
+ let cancelled = false;
+ void (async () => {
+ try {
+ const res = await statisticsApi.getColumnGuides(PUMPING_STATION_STATS_CD);
+ if (cancelled) return;
+ if (res.status !== "success" || !Array.isArray(res.data)) {
+ setColumnGuidesByEngCol({});
+ return;
+ }
+ const next: Record