침입수 즉시 제출 alert 복구 및 전역 완료 alert 감시 추가
This commit is contained in:
@@ -28,6 +28,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData, excelToRcptrInfoData } from '@/components/lib/utils'
|
||||
import BaseLayout from '../BaseLayout'
|
||||
import { useAnalysisCompletionWatcher } from '../useAnalysisCompletionWatcher'
|
||||
import { openAnalysisResultMapView } from '../analysisResultView'
|
||||
import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation';
|
||||
import { validateBasicSelectionsWithAlert } from '../../selectionValidation';
|
||||
@@ -94,6 +95,12 @@ const IrSetting = () => {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const { startWatching } = useAnalysisCompletionWatcher({
|
||||
buildStatusUrl: (analysisSn) => `v1/inftwtAnls/${analysisSn}/status`,
|
||||
successMessage: '침입수 분석이 완료되었습니다.',
|
||||
failureMessage: '침입수 분석이 실패했습니다. 로그를 확인해주세요.',
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -159,16 +166,21 @@ const IrSetting = () => {
|
||||
|
||||
|
||||
const registFile = async (analysisData: any) => {
|
||||
setIsLoadingResult(true);
|
||||
const data = await postMutiPart('v1/inftwtAnls/savewithfile', analysisData);
|
||||
if (data) {
|
||||
setIsLoadingResult(false);
|
||||
alert("분석 데이터가 성공적으로 제출되었습니다.");
|
||||
resetAllForms(); // 성공 시 폼 초기화 함수 호출
|
||||
} else {
|
||||
setIsLoadingResult(false);
|
||||
alert("분석 데이터 제출 중 오류가 발생했습니다.");
|
||||
if (!data) {
|
||||
throw new Error('분석 요청 접수 응답이 비어 있습니다.');
|
||||
}
|
||||
|
||||
const nextAnalysisSn = Number(data?.inftwtAnlsSn ?? 0);
|
||||
setIsLoadingResult(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
alert("분석 데이터가 성공적으로 제출되었습니다.");
|
||||
|
||||
if (Number.isFinite(nextAnalysisSn) && nextAnalysisSn > 0) {
|
||||
startWatching(nextAnalysisSn);
|
||||
}
|
||||
|
||||
resetAllForms();
|
||||
};
|
||||
|
||||
// 침입수 분석 실행 버튼 클릭 핸들러
|
||||
@@ -370,8 +382,10 @@ const IrSetting = () => {
|
||||
setIsLoadingResult(false);
|
||||
} catch (error) {
|
||||
setIsLoadingResult(false);
|
||||
console.error("파일 파싱 중 오류 발생:", error);
|
||||
alert("파일 처리 중 오류가 발생했습니다.");
|
||||
console.error("침입수 분석 요청 처리 중 오류 발생:", error);
|
||||
alert(error instanceof Error && error.message.trim()
|
||||
? error.message
|
||||
: "파일 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getData } from "@/components/lib/api";
|
||||
|
||||
type AnalysisStatusResponse = {
|
||||
scsYn?: string | null;
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
type UseAnalysisCompletionWatcherParams = {
|
||||
buildStatusUrl: (analysisSn: number) => string;
|
||||
successMessage: string;
|
||||
failureMessage: string;
|
||||
intervalMs?: number;
|
||||
};
|
||||
|
||||
type PendingAnalysisWatch = {
|
||||
analysisSn: number;
|
||||
failureMessage: string;
|
||||
key: string;
|
||||
statusUrl: string;
|
||||
successMessage: string;
|
||||
};
|
||||
|
||||
const DEFAULT_POLLING_INTERVAL_MS = 3000;
|
||||
const PENDING_ANALYSIS_WATCHES_STORAGE_KEY = "SN_PENDING_ANALYSIS_WATCHES";
|
||||
|
||||
const loadPendingAnalysisWatches = (): PendingAnalysisWatch[] => {
|
||||
if (typeof window === "undefined") {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(PENDING_ANALYSIS_WATCHES_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
console.error("분석 완료 감시 목록을 읽는 중 오류가 발생했습니다.", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const savePendingAnalysisWatches = (items: PendingAnalysisWatch[]) => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
window.localStorage.removeItem(PENDING_ANALYSIS_WATCHES_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(PENDING_ANALYSIS_WATCHES_STORAGE_KEY, JSON.stringify(items));
|
||||
};
|
||||
|
||||
const createWatchKey = (statusUrl: string, analysisSn: number) => `${statusUrl}::${analysisSn}`;
|
||||
|
||||
export function useAnalysisCompletionWatcher({
|
||||
buildStatusUrl,
|
||||
successMessage,
|
||||
failureMessage,
|
||||
}: UseAnalysisCompletionWatcherParams) {
|
||||
const startWatching = (analysisSn: number) => {
|
||||
if (typeof window === "undefined" || !Number.isFinite(analysisSn) || analysisSn <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusUrl = buildStatusUrl(analysisSn);
|
||||
const nextItem: PendingAnalysisWatch = {
|
||||
analysisSn,
|
||||
failureMessage,
|
||||
key: createWatchKey(statusUrl, analysisSn),
|
||||
statusUrl,
|
||||
successMessage,
|
||||
};
|
||||
|
||||
const nextItems = [
|
||||
...loadPendingAnalysisWatches().filter((item) => item.key !== nextItem.key),
|
||||
nextItem,
|
||||
];
|
||||
|
||||
savePendingAnalysisWatches(nextItems);
|
||||
};
|
||||
|
||||
const stopWatching = (analysisSn?: number) => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!analysisSn) {
|
||||
savePendingAnalysisWatches([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextItems = loadPendingAnalysisWatches().filter((item) => item.analysisSn !== analysisSn);
|
||||
savePendingAnalysisWatches(nextItems);
|
||||
};
|
||||
|
||||
return {
|
||||
startWatching,
|
||||
stopWatching,
|
||||
};
|
||||
}
|
||||
|
||||
export function GlobalAnalysisCompletionWatcher() {
|
||||
useEffect(() => {
|
||||
let intervalId: number | null = null;
|
||||
let isPolling = false;
|
||||
|
||||
const pollPendingAnalysis = async () => {
|
||||
if (isPolling || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.visibilityState !== "visible") {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentItems = loadPendingAnalysisWatches();
|
||||
if (currentItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPolling = true;
|
||||
|
||||
try {
|
||||
for (const item of currentItems) {
|
||||
const response = (await getData(item.statusUrl)) as AnalysisStatusResponse | null;
|
||||
if (!response) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const scsYn = String(response.scsYn ?? "").trim().toUpperCase();
|
||||
const message = String(response.message ?? "").trim();
|
||||
|
||||
if (scsYn !== "Y" && scsYn !== "N") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestItems = loadPendingAnalysisWatches();
|
||||
const exists = latestItems.some((latestItem) => latestItem.key === item.key);
|
||||
if (!exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savePendingAnalysisWatches(latestItems.filter((latestItem) => latestItem.key !== item.key));
|
||||
alert(message || (scsYn === "Y" ? item.successMessage : item.failureMessage));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("분석 완료 상태 조회 중 오류가 발생했습니다.", error);
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStorageOrVisibility = () => {
|
||||
void pollPendingAnalysis();
|
||||
};
|
||||
|
||||
intervalId = window.setInterval(() => {
|
||||
void pollPendingAnalysis();
|
||||
}, DEFAULT_POLLING_INTERVAL_MS);
|
||||
|
||||
window.addEventListener("storage", handleStorageOrVisibility);
|
||||
document.addEventListener("visibilitychange", handleStorageOrVisibility);
|
||||
void pollPendingAnalysis();
|
||||
|
||||
return () => {
|
||||
if (intervalId !== null) {
|
||||
window.clearInterval(intervalId);
|
||||
}
|
||||
window.removeEventListener("storage", handleStorageOrVisibility);
|
||||
document.removeEventListener("visibilitychange", handleStorageOrVisibility);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -129,4 +129,11 @@
|
||||
## 19. 2026-07-09 분석 대상지역 기본값 보정
|
||||
|
||||
- `ii/irsetting`, `ii/rdii`의 기본 선택값을 `경기도 / 양평군 / 양평`으로 맞췄다.
|
||||
- 공통 `CtpvSggPrcsFcltSelectBox`는 mount 시 부모 값을 강제로 `0`으로 되돌리던 초기화 흐름을 제거하고, 시설 목록 로딩 후 `defaultSwtrmtFcltNm` 이름 기준으로 기본 시설을 자동 선택하게 보강했다.
|
||||
- 공통 `CtpvSggPrcsFcltSelectBox`는 mount 시 부모 값을 강제로 `0`으로 되돌리던 초기화 흐름을 제거하고, 시설 목록 로딩 후 `defaultSwtrmtFcltNm` 이름 기준으로 기본 시설을 자동 선택하게 보강했다.
|
||||
- 이 보정으로 두 분석 화면 진입 직후 `하수처리시설명` select도 `양평` 기준으로 바로 보이도록 정리했다.
|
||||
## 20. 2026-07-09 침입수 분석 완료 alert 구현
|
||||
|
||||
- `ii/irsetting/page.tsx`는 더 이상 `savewithfile` 즉시 응답을 `분석 완료`로 간주하지 않는다.
|
||||
- 제출 성공 시 문구를 `분석 요청이 접수되었습니다. 완료 시 안내됩니다.`로 바꾸고, 응답으로 받은 `inftwtAnlsSn` 기준으로 상태 감시를 시작한다.
|
||||
- 신규 hook `ii/useAnalysisCompletionWatcher.ts`를 추가해 `GET /api/v1/inftwtAnls/{inftwtAnlsSn}/status` polling과 `Y/N` 종료 alert를 공통 처리한다.
|
||||
- 상태 추적 번호가 없으면 즉시 사용자에게 후속 결과목록 확인 안내를 띄운다.
|
||||
@@ -11,7 +11,7 @@ import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
|
||||
import { GlobalAnalysisCompletionWatcher } from "../../../app/(with-base-layout)/ii/useAnalysisCompletionWatcher";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
@@ -29,6 +29,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<GlobalAnalysisCompletionWatcher />
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
@@ -43,4 +44,4 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { AppProvider };
|
||||
export { AppProvider };
|
||||
Reference in New Issue
Block a user