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 | undefined; function getDashboardRole(): Promise { 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, }; }