import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES, type RoutePath, } from "@config/config_frontend"; import type { WorkflowStage } from "@ui/ui_template_workflow_layout"; import { fetchConfirmedSurface, fetchSurfaceStatus, } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { isProjectPreloaded, setPreloadTarget } from "./b_asset_cache"; import { navigateTo } from "./router"; export interface WorkflowState { project_id: string; /** 좌측 제목 줄 오른쪽에 붙는 프로젝트 이름 (2026-09-04 사용자 지시). */ project_name?: string | null; current_stage: number; stages: WorkflowStage[]; } export const WORKFLOW_STEP_ROUTES: readonly RoutePath[] = [ ROUTES.B03_FILE_INPUT, ROUTES.B04_PREPROCESS, ROUTES.B05_PROFILE, ROUTES.B06_SECTION, ROUTES.B07_DESIGN_DETAIL, ROUTES.B08_QUANTITY, ROUTES.B09_ESTIMATION, ]; /* 워크플로 상태는 한 화면을 여는 동안 준비 화면 가드와 페이지 본체가 각각 부른다 — 실측 B05 진입에서 두 번 나갔다(2026-09-06). 짧은 시간 동안 한 번만 부르고 나눠 쓴다. 단계가 바뀌는 자리(`goToWorkflowStage`)에서는 즉시 버린다. */ const WORKFLOW_CACHE_MS = 5000; let workflowCache: { at: number; projectId: string; value: Promise } | null = null; /** 워크플로 상태 캐시를 버린다 — 단계가 바뀌는 자리에서 부른다. */ export function clearWorkflowStateCache(): void { workflowCache = null; } export async function fetchWorkflowState(projectId: string): Promise { const now = Date.now(); if ( workflowCache && workflowCache.projectId === projectId && now - workflowCache.at < WORKFLOW_CACHE_MS ) { return workflowCache.value; } const value = (async (): Promise => { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/workflow-state`, { credentials: "include", }); if (!response.ok) { throw new Error(`Workflow state request failed: ${response.status}`); } const data = await response.json(); return data.workflow_state ?? data; })(); workflowCache = { at: now, projectId, value }; value.catch(() => clearWorkflowStateCache()); return value; } /** * 담아 둔 지표면 자료를 실제로 쓰는 화면. * * 이 화면들만 준비 화면(B11)을 거친다. 파일 입력·수량·상세설계·설계도서는 3D 지표면을 * 쓰지 않으므로, 확정 지표면이 없다고 준비 화면에서 막히면 안 된다 — 파일을 아직 올리지 * 않은 새 프로젝트에서 파일 입력 화면조차 못 들어가던 문제(2026-08-08). */ const PRELOAD_REQUIRED_ROUTES: readonly RoutePath[] = [ROUTES.B04_PREPROCESS, ROUTES.B05_PROFILE]; export function goToWorkflowStage(projectId: string, route: RoutePath): void { localStorage.setItem(CURRENT_PROJECT_ID_KEY, projectId); // 단계가 바뀌어 이동하는 자리다 — 캐시를 버려 다음 화면이 새 상태를 본다. clearWorkflowStateCache(); if (!PRELOAD_REQUIRED_ROUTES.includes(route)) { navigateTo(route); return; } void routeAfterPreloadCheck(projectId, route); } /** * 담아 둔 자료가 지금 확정본과 같은지 확인하고 화면을 정한다. * * 확정 구성을 묻는 요청은 수 KB라 이동할 때마다 물어도 부담이 없다. 담아 둔 표식과 다르면 * (관리자가 B04에서 다시 확정했거나, 새 브라우저이거나, 다른 프로젝트를 들렀다 온 경우) * 준비 화면(B11)을 거쳐 최신 자료를 담고 원래 가려던 화면으로 넘어간다(2026-08-01 사용자 지시). * 확인에 실패하면 준비 화면으로 보내 거기서 사유를 안내한다. */ async function routeAfterPreloadCheck(projectId: string, route: RoutePath): Promise { // 초기 설계 계산이 끝나기 전에는 담아 둔 자료가 있어도 들여보내지 않는다 — 반쯤 계산된 // 화면을 만지면 그 편집이 섞인 채 초기값이 찍힌다(2026-08-29 사용자 확정). 준비 화면이 // 끝날 때까지 기다렸다가 목표 화면으로 넘긴다. try { if ((await fetchSurfaceStatus(projectId)).current_stage === "initial_design") { setPreloadTarget(route); navigateTo(ROUTES.B11_LOADING); return; } } catch { /* 상태 조회 실패는 막지 않는다 — 종전 판정(확정 지표면 서명)으로 이어간다. */ } let signature: string | null = null; try { signature = (await fetchConfirmedSurface(projectId)).signature; } catch { signature = null; } if (signature && isProjectPreloaded(projectId, signature)) { navigateTo(route); return; } setPreloadTarget(route); navigateTo(ROUTES.B11_LOADING); }