Files
Aislo/ui_template/ui_template_workflow_layout.ts
T
eomsangdonandClaude Fable 5 ec9943417c feat(B07,B09): B07_Quantity 셸 페이지 신설 + B09_Estimation 접두사 정리 + 단계 순서 반영
- 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>
2026-08-08 10:15:07 +09:00

182 lines
6.1 KiB
TypeScript

import "./ui_template_workflow_layout.css";
import { ROUTES } from "@config/config_frontend";
import { t } from "./ui_template_locale";
import { createWorkflowOverlays } from "./ui_template_overlay";
import { navigateTo } from "../A00_Common/router";
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
let dashboardRolePromise: Promise<string> | undefined;
function getDashboardRole(): Promise<string> {
dashboardRolePromise ??= fetchDashboardMe().then((user) => user.role);
return dashboardRolePromise;
}
export interface WorkflowStage {
stage_no: number;
stage_key: string;
state: string;
}
export interface WorkflowLayoutOptions {
title: string;
steps: string[];
activeStep: number;
leftPanel?: HTMLElement;
mainContent: HTMLElement;
stages?: WorkflowStage[];
currentStage?: number;
routes?: readonly string[];
onStepClick?: (stepIndex: number, route: string) => void;
}
export interface WorkflowLayoutHandle {
root: HTMLElement;
setOptionsOpen: (isOpen: boolean) => void;
setProgressOpen: (isOpen: boolean) => void;
}
export interface StepBarOptions {
stages?: WorkflowStage[];
currentStage?: number;
routes?: readonly string[];
compact?: boolean;
orientation?: "horizontal" | "vertical";
icons?: readonly string[];
onStepClick?: (stepIndex: number, route: string) => void;
/** 목록 최상단에 대시보드 이동 버튼을 단다(진행단계 오버레이 전용). */
homeButton?: boolean;
}
export const WORKFLOW_STEP_ICONS = ["📁", "🗺️", "🛣️", "📐", "∑", "🏗️", "📄"] as const;
export function createStepBar(
steps: readonly string[],
activeStep: number,
options?: StepBarOptions,
): HTMLElement {
const bar = document.createElement("div");
bar.className = "ui-workflow-layout__steps";
if (options?.compact) bar.classList.add("is-compact");
if (options?.orientation === "vertical") bar.classList.add("is-vertical");
// 진행단계 오버레이(세로형) 최상단에 대시보드로 돌아가는 버튼을 둔다(2026-08-04 사용자
// 지시). 워크플로 단계가 아니라 탈출구라 stage 배지 없이 항상 활성이다.
if (options?.homeButton) {
const home = document.createElement("button");
home.type = "button";
home.className = "ui-workflow-layout__step is-enabled ui-workflow-layout__step--home";
const icon = document.createElement("span");
icon.className = "ui-workflow-layout__step-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "🏠";
const label = document.createElement("span");
label.className = "ui-workflow-layout__step-label";
label.textContent = t("Workflow_Step_Dashboard");
home.append(icon, label);
home.title = t("Workflow_Step_Dashboard");
home.addEventListener("click", () => navigateTo(ROUTES.B01_ACCOUNT));
bar.append(home);
}
steps.forEach((step, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "ui-workflow-layout__step";
if (index === activeStep) button.classList.add("is-active");
const iconText = options?.icons?.[index];
if (iconText) {
const icon = document.createElement("span");
icon.className = "ui-workflow-layout__step-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = iconText;
const label = document.createElement("span");
label.className = "ui-workflow-layout__step-label";
label.textContent = step;
button.append(icon, label);
} else {
button.textContent = step;
}
const stage =
options?.stages?.find((item) => item.stage_no === index) ?? options?.stages?.[index];
const hasStages = Boolean(options?.stages?.length);
const isBlockedState = stage?.state === "NOT_STARTED" || stage?.state === "STALE";
const isEnabled = !hasStages || !isBlockedState || stage?.stage_no === options?.currentStage;
button.classList.toggle("is-enabled", isEnabled);
button.disabled = !isEnabled;
if (index === 1) {
void getDashboardRole()
.then((role) => {
if (role !== "SYSTEM_ADMIN") {
button.classList.remove("is-enabled");
button.disabled = true;
button.title = "시스템 관리자 전용 단계입니다.";
}
})
.catch(() => undefined);
}
// state 기반 스타일링 (표시 전용)
if (stage) {
const state = stage.state;
button.classList.add(`state-${state.toLowerCase()}`);
if (state === "STALE") {
button.title = t("WF_State_Stale");
} else if (state === "FAILED") {
button.title = t("WF_State_Failed");
} else if (state === "COMPLETE") {
button.title = t("WF_State_Complete");
} else if (state === "IN_PROGRESS") {
button.title = t("WF_State_InProgress");
} else {
button.title = t("WF_State_NotStarted");
}
}
if (isEnabled && options?.routes?.[index]) {
button.addEventListener("click", () => {
options.onStepClick?.(index, options.routes![index]);
});
}
bar.append(button);
});
return bar;
}
export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLayoutHandle {
const root = document.createElement("div");
root.className = "ui-workflow-layout";
const body = document.createElement("div");
body.className = "ui-workflow-layout__body";
const main = document.createElement("main");
main.className = "ui-workflow-layout__main";
main.append(options.mainContent);
body.append(main);
const progressContent = createStepBar(options.steps, options.activeStep, {
stages: options.stages,
currentStage: options.currentStage,
routes: options.routes,
orientation: "vertical",
icons: WORKFLOW_STEP_ICONS,
onStepClick: options.onStepClick,
homeButton: true,
});
const overlays = createWorkflowOverlays({
title: options.title,
optionsContent: options.leftPanel,
progressContent,
onOptionsOpenChange: (isOpen) => root.classList.toggle("is-options-open", isOpen),
});
root.append(body, overlays.root);
return {
root,
setOptionsOpen: overlays.setTitleOpen,
setProgressOpen: overlays.setProgressOpen,
};
}