- B09_wf6_Estimation -> B09_Estimation (폴더·파일·라우트 키/슬러그)
- B07_Quantity_UI_Page.ts 신설: 워크플로우 셸 + 좌측 [확정] 버튼
(renderPendingWorkflow에 leftPanel 옵션 추가로 공용 셸 재사용)
- B07_Quantity_Router.py 신설: POST /api/projects/{id}/quantity/confirm
-> complete_stage(4) 전이만 수행(본문 미구현), main.py 등록
- STAGE_KEYS 재정의: FILE_INPUT/PREPROCESS/PROFILE/SECTION/QUANTITY/DESIGN_DETAIL/ESTIMATION
- 스텝바 라벨·아이콘 4↔5 스왑(수량산출이 4차, 상세설계가 5차), A02 소개 문구 스왑
- 라우터 테이블에 b07-quantity 등록(플레이스홀더 제거), locale 키 5종 추가
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
/* =============================================================================
|
|
* b_page_scaffold.ts
|
|
* B 그룹 페이지 공통 스캐폴드 — "헤더 + 준비 중 안내" 및 워크플로우 레이아웃 래퍼
|
|
*
|
|
* B03~B06 등 본문 미구현 페이지에서 헤더/푸터(공통 셸)를 제외한 콘텐츠 영역을
|
|
* 일관된 형태로 채우기 위한 헬퍼. 실제 본문은 0_old 참고하여 추후 구체화.
|
|
*
|
|
* 제약 준수 (frontend.md):
|
|
* - 문구는 호출측에서 locale 참조 후 문자열로 전달 (§3).
|
|
* - 공통 워크플로우 레이아웃(createWorkflowLayout) 재사용 (§2 3단 레이아웃).
|
|
* - 색상/간격은 theme.css 변수만 사용 (§1).
|
|
* ========================================================================== */
|
|
|
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
|
import {
|
|
fetchWorkflowState,
|
|
goToWorkflowStage,
|
|
WORKFLOW_STEP_ROUTES,
|
|
type WorkflowState,
|
|
} from "./b_workflow_nav";
|
|
|
|
/** locale 헬퍼 */
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
const SCAFFOLD_STYLE_ID = "b-page-scaffold-style";
|
|
|
|
/** "준비 중" 안내 블록 생성 (B_Content_Pending 문구 표시) */
|
|
function buildPendingBlock(): HTMLDivElement {
|
|
const block = document.createElement("div");
|
|
block.className = "b-pending";
|
|
const icon = document.createElement("div");
|
|
icon.className = "b-pending__icon";
|
|
icon.textContent = "🚧";
|
|
const msg = document.createElement("p");
|
|
msg.className = "b-pending__msg";
|
|
msg.textContent = L("B_Content_Pending");
|
|
block.append(icon, msg);
|
|
return block;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
* 1. 단순 헤더 + 준비 중 안내 (비(非)워크플로우 페이지: B03 등)
|
|
* -------------------------------------------------------------------------- */
|
|
export interface PendingContentOptions {
|
|
/** 페이지 루트 클래스 (예: "b03-file") */
|
|
pageClass: string;
|
|
title: string;
|
|
subtitle: string;
|
|
}
|
|
|
|
export function renderPendingContent(root: HTMLElement, opts: PendingContentOptions): void {
|
|
injectScaffoldStyles();
|
|
const page = document.createElement("div");
|
|
page.className = `b-scaffold ${opts.pageClass}`;
|
|
|
|
const header = document.createElement("div");
|
|
header.className = "b-scaffold__header";
|
|
const title = document.createElement("h1");
|
|
title.className = "b-scaffold__title";
|
|
title.textContent = opts.title;
|
|
const subtitle = document.createElement("p");
|
|
subtitle.className = "b-scaffold__subtitle";
|
|
subtitle.textContent = opts.subtitle;
|
|
header.append(title, subtitle);
|
|
|
|
page.append(header, buildPendingBlock());
|
|
root.append(page);
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
* 2. 워크플로우 셸(3단 레이아웃) + 준비 중 안내 (B04~B06 등)
|
|
* 좌측 패널·우측 뷰어 모두 준비 중 상태로 채운다.
|
|
* -------------------------------------------------------------------------- */
|
|
export interface PendingWorkflowOptions {
|
|
/** 상단 페이지 타이틀 (i18n 결과) */
|
|
title: string;
|
|
/** 진행 단계 라벨 배열 (i18n 결과) */
|
|
steps: string[];
|
|
/** 현재 활성 단계 인덱스 */
|
|
activeStep: number;
|
|
/** 본문은 준비 중이지만 좌측 패널만 먼저 붙일 때 (B07 수량 확정 버튼 등) */
|
|
leftPanel?: HTMLElement;
|
|
}
|
|
|
|
export async function renderPendingWorkflow(
|
|
root: HTMLElement,
|
|
opts: PendingWorkflowOptions,
|
|
): Promise<void> {
|
|
injectScaffoldStyles();
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
let workflowState: WorkflowState | undefined;
|
|
if (projectId) {
|
|
try {
|
|
workflowState = await fetchWorkflowState(projectId);
|
|
} catch {
|
|
/* 조회 실패 시 stages 미전달 → 전체 이동 허용 */
|
|
}
|
|
}
|
|
const layout = createWorkflowLayout({
|
|
title: opts.title,
|
|
steps: opts.steps,
|
|
activeStep: opts.activeStep,
|
|
leftPanel: opts.leftPanel,
|
|
mainContent: buildPendingBlock(),
|
|
stages: workflowState?.stages,
|
|
currentStage: workflowState?.current_stage,
|
|
routes: WORKFLOW_STEP_ROUTES,
|
|
onStepClick: (stepIndex) => {
|
|
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
|
},
|
|
});
|
|
layout.root.classList.add("b-scaffold-wf");
|
|
root.append(layout.root);
|
|
}
|
|
|
|
/** 워크플로우 스텝 라벨 7종 (locale 결과) — B03~B09 공용.
|
|
* 파일 입력(B03)부터 견적/문서(B09)까지 대시보드 워크플로우와 동일한 순서.
|
|
* 기존 WF_Step_* / B03_File_Title 키를 재사용 (중복 등록 방지, frontend.md §3). */
|
|
export function workflowSteps(): string[] {
|
|
return [
|
|
L("B03_File_Title"),
|
|
L("WF_Step_Surface"),
|
|
L("WF_Step_Route"),
|
|
L("WF_Step_ProfileCross"),
|
|
L("WF_Step_Quantity"),
|
|
L("WF_Step_DesignDetail"),
|
|
L("WF_Step_Estimation"),
|
|
];
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
* 스타일 주입 (theme.css 변수만 사용)
|
|
* -------------------------------------------------------------------------- */
|
|
const SCAFFOLD_CSS = `
|
|
.b-scaffold {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
padding: var(--spacing-40) var(--spacing-24);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-24);
|
|
}
|
|
.b-scaffold__header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-8);
|
|
}
|
|
.b-scaffold__title {
|
|
font-family: var(--font-display);
|
|
font-size: var(--text-heading);
|
|
color: var(--color-plum-velvet);
|
|
}
|
|
.b-scaffold__subtitle {
|
|
font-size: var(--text-body-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
.b-scaffold-wf { height: calc(100vh - 64px - 56px); }
|
|
|
|
.b-pending {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--spacing-16);
|
|
min-height: 240px;
|
|
padding: var(--spacing-40);
|
|
border: 1px dashed var(--color-border);
|
|
border-radius: var(--radius-cards);
|
|
background-color: var(--color-surface);
|
|
color: var(--color-text-muted);
|
|
}
|
|
.b-pending__icon { font-size: 40px; line-height: 1; }
|
|
.b-pending__msg { font-size: var(--text-body-sm); text-align: center; }
|
|
`;
|
|
|
|
function injectScaffoldStyles(): void {
|
|
if (document.getElementById(SCAFFOLD_STYLE_ID)) return;
|
|
const style = document.createElement("style");
|
|
style.id = SCAFFOLD_STYLE_ID;
|
|
style.textContent = SCAFFOLD_CSS;
|
|
document.head.append(style);
|
|
}
|