- 새 단추 꼴 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>
289 lines
11 KiB
JavaScript
289 lines
11 KiB
JavaScript
/* 요구 280-2 — 좌측 아래 [?] [<] [>] 헬퍼. `ui_template_help.ts` · `ui_template_workflow_layout.ts` 를
|
|
* 트랜스파일해 가짜 DOM 위에서 ① 주소마다 줄 차례 ② [<] [>] = 이웃 단계 단추 누름(꺼진 단계 = 알림만)을
|
|
* 돌려 JSON 한 벌로 냄. 확장자 `.cjs` 까닭은 `helper_design_view_parity.cjs` 머리와 같음.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
class FakeNode {
|
|
constructor(tag) {
|
|
this.tagName = tag;
|
|
this.children = [];
|
|
this.parent = null;
|
|
this.dataset = {};
|
|
this.listeners = {};
|
|
this.disabled = false;
|
|
this.isConnected = true;
|
|
this.title = "";
|
|
this._text = "";
|
|
this._classes = new Set();
|
|
const self = this;
|
|
this.classList = {
|
|
add: (...n) => n.forEach((x) => self._classes.add(x)),
|
|
remove: (...n) => n.forEach((x) => self._classes.delete(x)),
|
|
toggle: (n, f) =>
|
|
(f === undefined ? !self._classes.has(n) : !!f)
|
|
? self._classes.add(n)
|
|
: self._classes.delete(n),
|
|
contains: (n) => self._classes.has(n),
|
|
};
|
|
}
|
|
set className(v) {
|
|
this._classes = new Set(String(v).split(/\s+/).filter(Boolean));
|
|
}
|
|
get className() {
|
|
return [...this._classes].join(" ");
|
|
}
|
|
set textContent(v) {
|
|
this._text = String(v);
|
|
this.children = [];
|
|
}
|
|
get textContent() {
|
|
return this._text + this.children.map((c) => c.textContent).join("");
|
|
}
|
|
_adopt(kid) {
|
|
if (kid.parent) kid.parent.children = kid.parent.children.filter((c) => c !== kid);
|
|
kid.parent = this;
|
|
return kid;
|
|
}
|
|
append(...kids) {
|
|
kids.forEach((k) => this.children.push(this._adopt(k)));
|
|
}
|
|
prepend(...kids) {
|
|
this.children.unshift(...kids.map((k) => this._adopt(k)));
|
|
}
|
|
setAttribute() {}
|
|
addEventListener(type, handler) {
|
|
(this.listeners[type] ||= []).push(handler);
|
|
}
|
|
dispatchEvent(event) {
|
|
for (let node = this; node; node = event.bubbles ? node.parent : null)
|
|
(node.listeners[event.type] || []).forEach((h) => h(event));
|
|
}
|
|
click() {
|
|
if (!this.disabled) this.dispatchEvent({ type: "click", bubbles: true, target: this });
|
|
}
|
|
_matches(selector) {
|
|
if (selector === "[data-help-row]") return "helpRow" in this.dataset;
|
|
const [, cls, not] = /^\.([\w-]+)(?::not\(\.([\w-]+)\))?$/.exec(selector);
|
|
return this._classes.has(cls) && !(not && this._classes.has(not));
|
|
}
|
|
querySelector(selector) {
|
|
return this.querySelectorAll(selector)[0] ?? null;
|
|
}
|
|
querySelectorAll(selector) {
|
|
const out = [];
|
|
const walk = (n) => n.children.forEach((k) => (k._matches(selector) && out.push(k), walk(k)));
|
|
walk(this);
|
|
return out;
|
|
}
|
|
}
|
|
class CustomEvent {
|
|
constructor(type, init) {
|
|
Object.assign(this, { type, bubbles: !!init?.bubbles, detail: init?.detail });
|
|
}
|
|
}
|
|
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
|
|
globalThis.location = { hash: "" };
|
|
// 주소 바뀜 듣개 — B05 ↔ B06 은 페이지를 다시 안 세우고 [<] [>] 켬 · 끔만 다시 맞춤(496-1)
|
|
const hashListeners = new Set();
|
|
globalThis.window = {
|
|
addEventListener: (type, fn) => type === "hashchange" && hashListeners.add(fn),
|
|
removeEventListener: (type, fn) => type === "hashchange" && hashListeners.delete(fn),
|
|
};
|
|
const goHash = (route) => {
|
|
location.hash = `#/${route}`;
|
|
[...hashListeners].forEach((fn) => fn());
|
|
};
|
|
globalThis.CustomEvent = CustomEvent;
|
|
globalThis.HTMLButtonElement = FakeNode;
|
|
|
|
// ── 바깥 모듈 대역 ─────────────────────────────────────────────────────────────
|
|
const ROUTES = {
|
|
B01_ACCOUNT: "b01-account",
|
|
B03_FILE_INPUT: "b03-file-input",
|
|
B04_PREPROCESS: "b04-preprocess",
|
|
B05_PROFILE: "b05-profile",
|
|
B06_SECTION: "b06-section",
|
|
B07_DESIGN_DETAIL: "b07-design-detail",
|
|
B08_QUANTITY: "b08-quantity",
|
|
B09_ESTIMATION: "b09-estimation",
|
|
};
|
|
const toasts = [];
|
|
let role = "SYSTEM_ADMIN";
|
|
let help;
|
|
const STUBS = {
|
|
"@config/config_frontend": { ROUTES, API_BASE_URL: "" },
|
|
"./ui_template_locale": { t: (key) => key },
|
|
"./ui_template_modal": { openModal: () => undefined },
|
|
"./ui_template_elements": {
|
|
showToast: (message) => toasts.push(message),
|
|
el: (tag) => new FakeNode(tag),
|
|
createButton: ({ label, onClick }) => {
|
|
const button = new FakeNode("button");
|
|
button.className = "ui-btn";
|
|
button.textContent = label;
|
|
button.addEventListener("click", () => onClick && onClick());
|
|
return button;
|
|
},
|
|
},
|
|
// 진짜 겹판은 `splitSidebarActions` → `placeHelpButton` — 같은 차례로 흉내
|
|
"./ui_template_overlay": {
|
|
createWorkflowOverlays: ({ optionsContent, progressContent }) => {
|
|
const root = new FakeNode("div");
|
|
root.append(optionsContent, progressContent);
|
|
const actions = optionsContent._classes.has("ui-sidebar-actions")
|
|
? optionsContent
|
|
: optionsContent.querySelector(".ui-sidebar-actions");
|
|
help.placeHelpButton(actions);
|
|
return { root, setTitleOpen() {}, setProgressOpen() {} };
|
|
},
|
|
},
|
|
"../A00_Common/router": { navigateTo: () => undefined },
|
|
"../A00_Common/app_shell": { setDrawerSteps: () => undefined },
|
|
"../B01_Dashboard/B01_Dashboard_Api_Fetch": { fetchDashboardMe: async () => ({ role }) },
|
|
};
|
|
globalThis.__DEV = false;
|
|
|
|
function loadTs(file) {
|
|
const src = fs.readFileSync(file, "utf8").replace(/import\.meta\.env\.DEV/g, "globalThis.__DEV");
|
|
const js = ts.transpileModule(src, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
const localRequire = (request) => {
|
|
if (request.endsWith(".css")) return {};
|
|
if (STUBS[request]) return STUBS[request];
|
|
throw new Error(`대역 없음: ${request}`);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
help = loadTs(path.join(ROOT, "ui_template", "ui_template_help.ts"));
|
|
const layout = loadTs(path.join(ROOT, "ui_template", "ui_template_workflow_layout.ts"));
|
|
|
|
// ① 주소마다 줄 차례 — [data-help-row] 있는 줄 · 없는 줄(B08 · B09 꼴)
|
|
const rowOf = (route, withHelpRow) => {
|
|
location.hash = `#/${route}`;
|
|
const actions = new FakeNode("div");
|
|
actions.className = "ui-sidebar-actions";
|
|
const row = new FakeNode("div");
|
|
if (withHelpRow) {
|
|
row.dataset.helpRow = "";
|
|
actions.append(row);
|
|
}
|
|
const save = new FakeNode("button");
|
|
save.className = "ui-btn";
|
|
save.textContent = "저장";
|
|
(withHelpRow ? row : actions).append(save);
|
|
help.placeHelpButton(actions);
|
|
help.placeHelpButton(actions); // 두 번 불러도 한 벌
|
|
const kids = (withHelpRow ? row : actions).children;
|
|
return { labels: kids.map((k) => k.textContent), disabled: kids.map((k) => k.disabled) };
|
|
};
|
|
const ALL = [
|
|
"b03-file-input",
|
|
"b04-preprocess",
|
|
"b05-profile",
|
|
"b06-section",
|
|
"b07-design-detail",
|
|
"b08-quantity",
|
|
"b09-estimation",
|
|
"b09-print",
|
|
"m01-master-data",
|
|
"m02-master-template",
|
|
];
|
|
const rows = {};
|
|
for (const route of ALL) rows[route] = { helpRow: rowOf(route, true), plain: rowOf(route, false) };
|
|
|
|
// ② [<] [>] = 이웃 단계 누름
|
|
const STEP_ROUTES = ALL.slice(0, 7);
|
|
function page(route, { stages, dev = false, module = layout, current } = {}) {
|
|
globalThis.__DEV = dev;
|
|
goHash(route);
|
|
const calls = [];
|
|
const handle = module.createWorkflowLayout({
|
|
title: route,
|
|
steps: STEP_ROUTES,
|
|
activeStep: STEP_ROUTES.indexOf(route),
|
|
mainContent: new FakeNode("div"),
|
|
routes: STEP_ROUTES,
|
|
stages,
|
|
currentStage: current ?? STEP_ROUTES.indexOf(route),
|
|
onStepClick: (index, to) => calls.push(`${index}:${to}`),
|
|
});
|
|
const navButton = (step) =>
|
|
handle.root
|
|
.querySelectorAll(".ui-step-nav-btn")
|
|
.find((b) => b.dataset.stepNav === String(step));
|
|
const press = (step) => {
|
|
toasts.length = 0;
|
|
navButton(step).click();
|
|
return { calls: calls.splice(0), toasts: [...toasts] };
|
|
};
|
|
const stepButton = (index) =>
|
|
handle.root.querySelectorAll(".ui-workflow-layout__step:not(.ui-workflow-layout__step--home)")[
|
|
index
|
|
];
|
|
return { handle, press, navButton, stepButton };
|
|
}
|
|
|
|
(async () => {
|
|
const nav = {};
|
|
const b05 = page("b05-profile");
|
|
nav.b05Prev = b05.press(-1);
|
|
nav.b05Next = b05.press(1);
|
|
nav.prevDisabled = { b05: b05.navButton(-1).disabled };
|
|
// B05 → B06 = 같은 페이지 — 켜진 칸만 갈아 끼움(design_page 의 setActiveStep) · [<] 는 주소 따라 켬
|
|
goHash("b06-section");
|
|
b05.handle.setActiveStep(3);
|
|
nav.prevDisabled.b06 = b05.navButton(-1).disabled;
|
|
nav.b06Prev = b05.press(-1);
|
|
nav.b06Next = b05.press(1);
|
|
goHash("b05-profile");
|
|
b05.handle.setActiveStep(2);
|
|
nav.prevDisabled.back = b05.navButton(-1).disabled;
|
|
const b09 = page("b09-estimation");
|
|
nav.b09Prev = b09.press(-1);
|
|
nav.b09Next = b09.press(1);
|
|
// 운영 — 막힌 단계(NOT_STARTED) = 누르지 않고 그 단추 title 알림
|
|
const stages = STEP_ROUTES.map((_, i) => ({
|
|
stage_no: i,
|
|
stage_key: String(i),
|
|
state: i >= 4 ? "NOT_STARTED" : "COMPLETE",
|
|
}));
|
|
const blocked = page("b06-section", { stages });
|
|
nav.blockedNext = blocked.press(1);
|
|
// [저장] 뒤 서버 단계로 다시 칠함 — 시작함(IN_PROGRESS)이면 켬
|
|
blocked.handle.setStages(
|
|
stages.map((s) => ({ ...s, state: s.stage_no === 4 ? "IN_PROGRESS" : s.state })),
|
|
);
|
|
nav.repaintNext = blocked.press(1);
|
|
nav.repaintClass = blocked.stepButton(4).className;
|
|
// STALE = 색만 · 막지 않음(요구 300 B1)
|
|
const stale = page("b06-section", {
|
|
stages: stages.map((s) => ({ ...s, state: s.stage_no >= 4 ? "STALE" : s.state })),
|
|
});
|
|
nav.staleNext = stale.press(1);
|
|
nav.staleClass = stale.stepButton(4).className;
|
|
// B05 · B06 화면 = 2 · 3 · 4 늘 켬(운영 · 체인 직후 NOT_STARTED 라도) · B08 은 막힘
|
|
const chain = page("b06-section", { stages, current: [2, 3, 4] });
|
|
nav.listNext = chain.press(1);
|
|
nav.listB08 = chain.stepButton(5).disabled;
|
|
// 일반 사용자 — B05 [<](B04) = 관리자 전용 알림(역할은 모듈이 한 번 물어 기억 → 새로 읽음)
|
|
role = "USER";
|
|
const fresh = loadTs(path.join(ROOT, "ui_template", "ui_template_workflow_layout.ts"));
|
|
const user = page("b06-section", { dev: true, module: fresh });
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
// B06 [<] = B05(같은 페이지) · B04 단계 단추 = 관리자 전용 잠금(다시 칠해도 남음)
|
|
nav.userPrev = user.press(-1);
|
|
const lockOf = (b) => ({ disabled: b.disabled, title: b.title });
|
|
nav.userB04 = lockOf(user.stepButton(1));
|
|
user.handle.setStages(stages);
|
|
nav.userB04Repaint = lockOf(user.stepButton(1));
|
|
process.stdout.write(JSON.stringify({ rows, nav }));
|
|
})();
|