Files
Aislo/A00_Common/b_workflow_nav.ts
T
eomsangdonandClaude Opus 5 df2dedcbc4 perf: 화면 진입 중복 호출 정리 — 세션·워크플로 상태·구조물 이관
실측(B05 진입): auth/session 4회, workflow-state 2회, 구조물 이관 POST 가 진입할
때마다 나갔다.

- auth/session: 라우터 가드·상단 바·워크플로 확인이 각각 부르던 것을 5초 캐시로
  묶음. 로그인·로그아웃에서 즉시 버린다.
- workflow-state: 공용 창구 하나로 모으고 같은 방식으로 캐시. 상단 오버레이가
  따로 부르던 경로를 그 창구로 돌림. 단계 이동에서 캐시를 버린다.
- 구 구조물 측점 이관 POST: 탭 수명 동안 노선마다 한 번만.

실측 결과: B05 첫 진입 20 → 18건(핵심 API 10건), B06→B05 이동 7건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 15:28:20 +09:00

122 lines
4.7 KiB
TypeScript

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<WorkflowState> } | null = null;
/** 워크플로 상태 캐시를 버린다 — 단계가 바뀌는 자리에서 부른다. */
export function clearWorkflowStateCache(): void {
workflowCache = null;
}
export async function fetchWorkflowState(projectId: string): Promise<WorkflowState> {
const now = Date.now();
if (
workflowCache &&
workflowCache.projectId === projectId &&
now - workflowCache.at < WORKFLOW_CACHE_MS
) {
return workflowCache.value;
}
const value = (async (): Promise<WorkflowState> => {
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<void> {
// 초기 설계 계산이 끝나기 전에는 담아 둔 자료가 있어도 들여보내지 않는다 — 반쯤 계산된
// 화면을 만지면 그 편집이 섞인 채 초기값이 찍힌다(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);
}