feat(496-1 B05·B06 단추 줄): [확정] 지움 · 차례 [?] [<] [>] [저장] [초기화] · B06 [>] = 확정 뒤 B07
- 새 단추 꼴 danger-ghost(테두리 · 글 빨강 · 올리면 채움) · [초기화]만 - B05 [<] 흐림 · 주소 바뀔 때마다 [<] [>] 다시 맞춤 - 단계 판 B07~B09 · B06 [>] = 확정 뒤 그 주소 · 실패 · 자료 서기 전 = 머묾 - 노선 바뀐 채 확정 = 저장 뒤 물음 [머묾] / [B07 로](새 노선으로 확정 뒤 이동) - 막힘 = NOT_STARTED 만(STALE 은 색만) · B05·B06 화면 2 · 3 · 4 칸 늘 켬 · [저장] 뒤 단계 색 다시 읽음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,8 @@ export interface WorkflowLayoutOptions {
|
||||
leftPanel?: HTMLElement;
|
||||
mainContent: HTMLElement;
|
||||
stages?: WorkflowStage[];
|
||||
currentStage?: number;
|
||||
/** 막힌(NOT_STARTED) 단계라도 켤 칸 — 수 하나 또는 목록(B05 · B06 = 2 · 3 · 4 · 496-1) */
|
||||
currentStage?: number | readonly number[];
|
||||
routes?: readonly string[];
|
||||
onStepClick?: (stepIndex: number, route: string) => void;
|
||||
}
|
||||
@@ -39,11 +40,13 @@ export interface WorkflowLayoutHandle {
|
||||
/** 한 페이지 두 보기(B05 · B06) — 보기가 바뀌면 제목 · 켜진 단계만 갈아 끼운다. */
|
||||
setTitle: (title: string) => void;
|
||||
setActiveStep: (stepIndex: number) => void;
|
||||
/** [저장] · [>] 뒤 서버 단계 상태로 색 · 켬을 다시 맞춘다(496-1). */
|
||||
setStages: (stages: WorkflowStage[]) => void;
|
||||
}
|
||||
|
||||
export interface StepBarOptions {
|
||||
stages?: WorkflowStage[];
|
||||
currentStage?: number;
|
||||
currentStage?: number | readonly number[];
|
||||
routes?: readonly string[];
|
||||
compact?: boolean;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
@@ -53,6 +56,8 @@ export interface StepBarOptions {
|
||||
homeButton?: boolean;
|
||||
}
|
||||
|
||||
type StepBarElement = HTMLElement & { repaintSteps?: (stages: WorkflowStage[]) => void };
|
||||
|
||||
export const WORKFLOW_STEP_ICONS = ["📁", "🗺️", "🛣️", "📐", "∑", "🏗️", "📄"] as const;
|
||||
|
||||
export function createStepBar(
|
||||
@@ -62,6 +67,9 @@ export function createStepBar(
|
||||
): HTMLElement {
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "ui-workflow-layout__steps";
|
||||
// 단계 단추 다시 칠하기(`setStages`) — 단추 차례대로
|
||||
const painters: Array<(stages: WorkflowStage[]) => void> = [];
|
||||
(bar as StepBarElement).repaintSteps = (stages) => painters.forEach((paint) => paint(stages));
|
||||
if (options?.compact) bar.classList.add("is-compact");
|
||||
if (options?.orientation === "vertical") bar.classList.add("is-vertical");
|
||||
// 진행단계 오버레이(세로형) 최상단에 대시보드로 돌아가는 버튼을 둔다(2026-08-04 사용자
|
||||
@@ -101,59 +109,74 @@ export function createStepBar(
|
||||
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";
|
||||
// ⚠ 개발 전용 — 막힌 단계도 눌러서 들어간다 (2026-09-09 사용자 지시).
|
||||
// B08 의 「확정 없이 다음으로」 단추가 **B08 화면 안에** 있어서, B07 에서 B08 로 못
|
||||
// 넘어가면 그 단추에 닿을 길이 없었다(닫힌 문 안에 열쇠가 있는 꼴).
|
||||
// ⚠ **잠금을 푸는 것이 아니라 가기만 한다** — 값이 없어 「미확보」로 뜨는 것이 정상이고,
|
||||
// 실제 우회는 B08 의 그 단추가 서버(`common_util_dev_unlock`)로 한다.
|
||||
// ⚠ **서버는 그대로 막혀 있다** — 운영에서는 `ENVIRONMENT` 로 거절한다.
|
||||
const devBypass = import.meta.env.DEV;
|
||||
const isEnabled =
|
||||
devBypass || !hasStages || !isBlockedState || stage?.stage_no === options?.currentStage;
|
||||
button.classList.toggle("is-enabled", isEnabled);
|
||||
button.disabled = !isEnabled;
|
||||
const currents = ([] as number[]).concat(options?.currentStage ?? []);
|
||||
let adminOnly = false; // B04 = 시스템 관리자만
|
||||
const paint = (stages: WorkflowStage[] | undefined): void => {
|
||||
const stage = stages?.find((item) => item.stage_no === index) ?? stages?.[index];
|
||||
const hasStages = Boolean(stages?.length);
|
||||
// 막힘 = 시작 전만 — STALE(앞 단계 바뀜)은 색만 「다시 확인」 · 막지 않음(요구 300 B1)
|
||||
const isBlockedState = stage?.state === "NOT_STARTED";
|
||||
// ⚠ 개발 전용 — 막힌 단계도 눌러서 들어간다 (2026-09-09 사용자 지시).
|
||||
// B08 의 「확정 없이 다음으로」 단추가 **B08 화면 안에** 있어서, B07 에서 B08 로 못
|
||||
// 넘어가면 그 단추에 닿을 길이 없었다(닫힌 문 안에 열쇠가 있는 꼴).
|
||||
// ⚠ **잠금을 푸는 것이 아니라 가기만 한다** — 값이 없어 「미확보」로 뜨는 것이 정상이고,
|
||||
// 실제 우회는 B08 의 그 단추가 서버(`common_util_dev_unlock`)로 한다.
|
||||
// ⚠ **서버는 그대로 막혀 있다** — 운영에서는 `ENVIRONMENT` 로 거절한다.
|
||||
const devBypass = import.meta.env.DEV;
|
||||
const isEnabled =
|
||||
!adminOnly &&
|
||||
(devBypass || !hasStages || !isBlockedState || currents.includes(stage?.stage_no ?? -1));
|
||||
button.classList.toggle("is-enabled", isEnabled);
|
||||
button.disabled = !isEnabled;
|
||||
button.title = "";
|
||||
|
||||
// state 기반 스타일링 (표시 전용)
|
||||
button.className
|
||||
.split(/\s+/)
|
||||
.filter((name) => name.startsWith("state-"))
|
||||
.forEach((name) => button.classList.remove(name));
|
||||
button.classList.remove("is-dev-bypass");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// 개발 우회로 열어 둔 단계는 **그렇다고 말해 준다** — 조용히 열면 다음 사람이
|
||||
// 「왜 값이 없나」로 헤맨다(B08 의 안내 줄과 같은 뜻).
|
||||
if (devBypass && isBlockedState && !adminOnly) {
|
||||
button.classList.add("is-dev-bypass");
|
||||
button.title = `${button.title} (개발: 확정 전이라 값이 비어 보일 수 있습니다)`;
|
||||
}
|
||||
if (adminOnly) button.title = "시스템 관리자 전용 단계입니다.";
|
||||
};
|
||||
let painted = options?.stages;
|
||||
paint(painted);
|
||||
painters.push((stages) => paint((painted = stages)));
|
||||
|
||||
if (options?.routes?.[index] === ROUTES.B04_PREPROCESS) {
|
||||
void getDashboardRole()
|
||||
.then((role) => {
|
||||
if (role !== "SYSTEM_ADMIN") {
|
||||
button.classList.remove("is-enabled");
|
||||
button.disabled = true;
|
||||
button.title = "시스템 관리자 전용 단계입니다.";
|
||||
adminOnly = true;
|
||||
paint(painted);
|
||||
}
|
||||
})
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
// 개발 우회로 열어 둔 단계는 **그렇다고 말해 준다** — 조용히 열면 다음 사람이
|
||||
// 「왜 값이 없나」로 헤맨다(B08 의 안내 줄과 같은 뜻).
|
||||
if (devBypass && isBlockedState) {
|
||||
button.classList.add("is-dev-bypass");
|
||||
button.title = `${button.title} (개발: 확정 전이라 값이 비어 보일 수 있습니다)`;
|
||||
}
|
||||
|
||||
if (isEnabled && options?.routes?.[index]) {
|
||||
// 꺼진 단추는 누름이 안 감 — 켬 · 끔이 뒤에 바뀌어도 듣개 하나
|
||||
if (options?.routes?.[index]) {
|
||||
button.addEventListener("click", () => {
|
||||
options.onStepClick?.(index, options.routes![index]);
|
||||
});
|
||||
@@ -225,5 +248,6 @@ export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLa
|
||||
},
|
||||
setActiveStep: (stepIndex) =>
|
||||
steps.forEach((step, index) => step.classList.toggle("is-active", index === stepIndex)),
|
||||
setStages: (stages) => (progressContent as StepBarElement).repaintSteps?.(stages),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user