feat(B03,B05,B06): 초기 계산 끝까지 업로드 잠금 + 자료 없으면 대시보드로 (결함 3·6 화면측)
업로드 잠금(결함 3) - 종전에는 전처리(B04)만 끝나면 버튼이 풀려, 노선·횡단이 도는 동안 자료를 또 올릴 수 있었다. isInitialPipelineRunning()/pollInitialPipeline()을 두어 **횡단 단계가 열릴 때까지**(3단계가 NOT_STARTED를 벗어날 때까지) 잠근다. - relockWhileInitialPipelineRuns(): 새로고침·재진입해도 계산이 도는 중이면 다시 잠그고 스피너를 붙인다. 잠금이 화면 상태로만 있으면 새로고침 한 번에 풀렸다. - 보관함 연결 경로도 같은 대기 흐름을 탄다. 자료 없으면 대시보드(결함 6) - b_missing_data_guard 신설 — 근거 자료가 없으면 한 줄 안내 후 B01로 보낸다. 새 자료가 올라오면 서버가 옛 산출물을 지우므로, 열어 둔 뒷단계 화면은 빈 화면이 아니라 대시보드로 나가야 한다(2026-08-08 사용자 지시). - B05: 확정 지표면이 없으면 이동. B06: 노선 또는 종단면이 없으면 이동. 검증: 자료가 없는 신규 프로젝트로 B05 진입 → 안내 후 #/b01-account 로 이동함을 헤드리스 크롬으로 확인. typecheck·prettier 통과, 정적 번들 재빌드. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { PROGRESS_UPDATE_INTERVAL_MS, UPLOAD_CHUNK_SIZE_MB } from "@config/config_frontend";
|
||||
import { fetchWorkflowState, type WorkflowState } from "../A00_Common/b_workflow_nav";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -168,6 +169,48 @@ export async function uploadOneFile(
|
||||
* 지표면 분석(WF1)이 끝날 때까지 5초 간격으로 확인한다.
|
||||
* 자동 확정이 보류되면 `onHold`로 사유를 넘기고 대기를 끝낸다.
|
||||
*/
|
||||
/**
|
||||
* 초기 자동 계산이 아직 도는 중인지 — 업로드 버튼을 잠가 둘지 판단하는 기준.
|
||||
*
|
||||
* 자료를 올리면 서버가 전처리(B04) → 노선(B05) → 횡단(B06)까지 혼자 이어서 돈다. 그 사이에
|
||||
* 자료를 또 올리면 분석 두 개가 같은 산출물 자리에서 부딪힌다. 그래서 **횡단 단계가
|
||||
* 시작될 때까지**를 "도는 중"으로 본다 — 체인이 B06을 확정하면 3단계가 NOT_STARTED를
|
||||
* 벗어난다(2026-08-08 사용자 지시).
|
||||
*
|
||||
* 전처리가 실패했으면 더 기다릴 게 없으므로 잠금을 푼다.
|
||||
*/
|
||||
export function isInitialPipelineRunning(state: WorkflowState | undefined): boolean {
|
||||
if (!state?.stages?.length) return false;
|
||||
const stageAt = (stageNo: number) => state.stages.find((stage) => stage.stage_no === stageNo);
|
||||
const fileInput = stageAt(0);
|
||||
const preprocess = stageAt(1);
|
||||
const section = stageAt(3);
|
||||
if (fileInput?.state !== "COMPLETE") return false;
|
||||
if (preprocess?.state === "FAILED") return false;
|
||||
return section?.state === "NOT_STARTED";
|
||||
}
|
||||
|
||||
/** 초기 자동 계산(B04~B06)이 끝날 때까지 기다린다. 끝나면 true. */
|
||||
export async function pollInitialPipeline(
|
||||
projectId: string,
|
||||
onHold: (message: string) => void,
|
||||
maxAttempts = 360,
|
||||
): Promise<boolean> {
|
||||
const analysisDone = await pollWF1Analysis(projectId, onHold, maxAttempts);
|
||||
if (!analysisDone) return false;
|
||||
// 전처리가 끝나도 노선·횡단이 이어서 돈다 — 3단계가 열릴 때까지 더 기다린다.
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const state = await fetchWorkflowState(projectId);
|
||||
if (!isInitialPipelineRunning(state)) return true;
|
||||
} catch {
|
||||
/* 일시적인 조회 실패는 다음 주기에 다시 확인한다. */
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function pollWF1Analysis(
|
||||
projectId: string,
|
||||
onHold: (message: string) => void,
|
||||
|
||||
Reference in New Issue
Block a user