183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
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 = "🛠️ <span>Option ▾</span>";
|
|
|
|
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 };
|
|
}
|