import "./ui_template_workflow_layout.css"; 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[]; routes?: string[]; onStepClick?: (stepIndex: number, route: string) => void; onMenuToggle?: (isOpen: boolean) => void; onHeaderToggle?: (isVisible: boolean) => void; } export interface WorkflowLayoutHandle { root: HTMLElement; setMenuOpen: (isOpen: boolean) => void; setHeaderVisible: (isVisible: boolean) => void; } function createStepBar( steps: readonly string[], activeStep: number, options?: { stages?: WorkflowStage[]; routes?: string[]; onStepClick?: (stepIndex: number, route: string) => void; }, ): HTMLElement { const bar = document.createElement("div"); bar.className = "ui-workflow-layout__steps"; 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"); button.textContent = step; // DB state 기반 활성화 로직 let enabled = false; if (options?.stages && options.stages.length > 0) { enabled = index === 0 || options.stages[index - 1]?.state === "COMPLETE"; } else { enabled = index <= activeStep; } button.disabled = !enabled; if (enabled) { button.classList.add("is-enabled"); } // state 기반 스타일링 if (options?.stages && options.stages[index]) { const state = options.stages[index].state; button.classList.add(`state-${state.toLowerCase()}`); if (state === "STALE") { button.title = "Stale (하위 단계 변경으로 무효화됨)"; } else if (state === "FAILED") { button.title = "Failed (실패)"; } else if (state === "COMPLETE") { button.title = "Complete (완료)"; } else if (state === "IN_PROGRESS") { button.title = "In Progress (진행 중)"; } else { button.title = "Not Started (미실행)"; } } // 클릭 이벤트 if (enabled && options?.routes && 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 header = document.createElement("header"); header.className = "ui-workflow-layout__header"; const menuButton = document.createElement("button"); menuButton.type = "button"; menuButton.className = "ui-workflow-layout__menu-button"; menuButton.setAttribute("aria-label", "Toggle settings panel"); menuButton.innerHTML = "🛠️ Option ▾"; const titleWrap = document.createElement("div"); titleWrap.className = "ui-workflow-layout__title-wrap"; const title = document.createElement("h2"); title.className = "ui-workflow-layout__title"; title.textContent = options.title; titleWrap.append( title, createStepBar(options.steps, options.activeStep, { stages: options.stages, routes: options.routes, onStepClick: options.onStepClick, }), ); const headerHideButton = document.createElement("button"); headerHideButton.type = "button"; headerHideButton.className = "ui-workflow-layout__header-hide"; headerHideButton.setAttribute("aria-label", "Hide header"); headerHideButton.textContent = "▲"; header.append(titleWrap, headerHideButton); // 헤더 숨김 시 표시되는 복원 화살표 const headerRevealButton = document.createElement("button"); headerRevealButton.type = "button"; headerRevealButton.className = "ui-workflow-layout__header-reveal"; headerRevealButton.setAttribute("aria-label", "Show header"); headerRevealButton.textContent = "▼"; const body = document.createElement("div"); body.className = "ui-workflow-layout__body"; const dropdownPanel = document.createElement("div"); dropdownPanel.className = "ui-workflow-layout__dropdown-panel"; dropdownPanel.append(options.leftPanel); const main = document.createElement("main"); main.className = "ui-workflow-layout__main"; main.style.position = "relative"; main.append(menuButton, options.mainContent); body.append(dropdownPanel, main); root.append(header, headerRevealButton, body); function setMenuOpen(isOpen: boolean): void { root.classList.toggle("is-menu-open", isOpen); menuButton.setAttribute("aria-expanded", String(isOpen)); const icon = menuButton.querySelector("span"); if (icon) { icon.textContent = isOpen ? "Option ▴" : "Option ▾"; } options.onMenuToggle?.(isOpen); } function setHeaderVisible(isVisible: boolean): void { root.classList.toggle("is-header-hidden", !isVisible); headerHideButton.setAttribute("aria-expanded", String(isVisible)); options.onHeaderToggle?.(isVisible); } menuButton.addEventListener("click", (e) => { e.stopPropagation(); setMenuOpen(!root.classList.contains("is-menu-open")); }); document.addEventListener("click", (e) => { if (root.classList.contains("is-menu-open")) { const target = e.target as HTMLElement; if (!dropdownPanel.contains(target) && !menuButton.contains(target)) { setMenuOpen(false); } } }); headerHideButton.addEventListener("click", () => setHeaderVisible(false)); headerRevealButton.addEventListener("click", () => setHeaderVisible(true)); setMenuOpen(false); setHeaderVisible(true); return { root, setMenuOpen, setHeaderVisible }; }