/* 298-2 B08 · B09 좌측 아래 단추 줄 — 진짜 `B08_Quantity_UI_Options.ts` · `B08_Quantity_UI_Page.ts` · * `B09_Estimation_UI_Shell.ts` 를 트랜스파일해 가짜 DOM · 가짜 의존 위에서 돌리고 값을 JSON 으로 냄. * [초기화] = 확인 창 → POST reset · [>] = 저장 → POST complete → 다음 길 · 실패면 머묾. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); // 무엇이든 받아 주는 빈 값 — 부르면 또 빈 값 · 펼치면 비어 있음 · 글자로는 "" function any() { const store = {}; return new Proxy(function () {}, { get(_t, k) { if (k in store) return store[k]; if (k === "then") return undefined; if (k === Symbol.iterator) return function* () {}; if (k === Symbol.toPrimitive) return () => ""; return (store[k] = any()); }, set(_t, k, v) { store[k] = v; return true; }, apply: () => any(), construct: () => any(), }); } class Node { constructor(tag = "div") { this.tagName = tag.toUpperCase(); this.children = []; this.parent = null; this.dataset = {}; this.style = {}; this.listeners = {}; this.classList = { add() {}, toggle() {}, remove() {}, contains: () => false }; } get isConnected() { let n = this; while (n.parent) n = n.parent; return n.isRoot === true; } append(...kids) { kids.forEach((k) => { if (k instanceof Node) k.parent = this; this.children.push(k); }); } replaceChildren(...kids) { this.children.forEach((k) => k instanceof Node && (k.parent = null)); this.children = []; this.append(...kids); } setAttribute() {} querySelector() { return null; } querySelectorAll() { return []; } addEventListener(name, fn) { (this.listeners[name] ??= []).push(fn); } } global.document = { createElement: (tag) => new Node(tag) }; function load(rel, stubs) { const src = fs .readFileSync(path.join(ROOT, rel), "utf8") .replace(/import\.meta\.glob\(/g, "(() => ({}))("); const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const mod = { exports: {} }; const req = (id) => (id in stubs ? stubs[id] : id.endsWith(".css") ? {} : any()); new Function("exports", "module", "require", js)(mod.exports, mod, req); return mod.exports; } const flush = async () => { for (let k = 0; k < 8; k++) await new Promise((r) => setTimeout(r, 0)); }; const calls = []; const buttons = []; // 만든 단추 — { variant, node } let layoutOpts = null; let confirmAnswer = false; const store = new Map(); // 가짜 세션 캐시 const common = { "@ui/ui_template_workflow_layout": { createWorkflowLayout: (opts) => { layoutOpts = opts; const root = new Node(); root.append(opts.leftPanel, opts.mainContent); return { root }; }, }, "@ui/ui_template_elements": { createButton: (o) => { const node = Object.assign(new Node("button"), { onClick: o.onClick, variant: o.variant }); buttons.push(node); return node; }, createInputField: () => ({ input: new Node("input"), root: new Node() }), showToast: (_text, kind) => calls.push(`toast:${kind}`), showLoadingOverlay: () => calls.push("lock"), hideLoadingOverlay: () => calls.push("unlock"), showConfirmDialog: async () => (calls.push("confirm"), confirmAnswer), }, "@ui/ui_template_collapsible": { attachCollapsible() {}, createPanelCard: () => new Node() }, "@ui/ui_template_locale": { ui_locales: any(), currentLanguageIndex: 0 }, "@config/config_frontend": { CURRENT_PROJECT_ID_KEY: "pid", API_BASE_URL: "/api" }, "../A00_Common/b_page_scaffold": { workflowSteps: () => [] }, "../A00_Common/b_workflow_nav": { WORKFLOW_STEP_ROUTES: ["b03", "b04", "b05", "b06", "b07", "b08", "b09"], // 520-3 공용 페이지 넘김 — 단계 차례 · false · 던짐 = 머묾(잠금 · 진행 글은 test_520_3) moveStage: async (_p, target, steps) => { for (const step of steps) try { if ((await step.run()) === false) return false; } catch { calls.push("toast:error"); return false; } if (typeof target === "function") target(); else calls.push(`go:${target}`); return true; }, }, "../A00_Common/b_page_state": { readState: (name) => store.get(name) ?? null, writeState: (name, v) => store.set(name, v), clearState: (name) => store.delete(name), }, }; global.localStorage = { getItem: () => "P1" }; const take = () => calls.splice(0).filter((c) => !c.startsWith("toast:success")); // B08 좌측 옵션 [저장] [초기화] async function b08Options() { let fail = false; const m = load("B08_Quantity/B08_Quantity_UI_Options.ts", { ...common, "./B08_Quantity_UI_Hydro": { request: async (url, init) => { const method = init?.method ?? "GET"; if (method !== "GET") calls.push(`req:${method}:${url}`); if (fail && method === "POST") throw new Error("500"); return { one_pipe_length_m: 6, rock_ratios_pct: { 연암: 90 }, material_surcharge_pct: {} }; }, }, "./B08_Quantity_UI_Milestone_State": { saveMilestone: async () => calls.push("req:milestone"), loadMilestone: async (_p, force) => calls.push(`milestone:${force}`), }, }); buttons.length = 0; const panel = m.createOptionsPanel("P1", () => calls.push("redraw")); await flush(); const actions = panel.panel.children.find((c) => c.className === "ui-sidebar-actions"); const out = { order: actions.children.map((b) => b.variant) }; const reset = actions.children[1]; const drafts = ["b08-options", "b08-materials", "b08-hydro", "b08-milestone"]; const fill = () => drafts.forEach((d) => store.set(d, { x: 1 })); const press = async () => { take(); reset.onClick(); await flush(); return take(); }; fill(); confirmAnswer = false; out.cancel = [await press(), drafts.filter((d) => store.has(d))]; confirmAnswer = true; out.ok = [await press(), drafts.filter((d) => store.has(d))]; fill(); fail = true; out.fail = [await press(), drafts.filter((d) => store.has(d))]; store.clear(); return out; } // B08 진행 단계 · [>] — B09 쪽 = 저장 → POST complete → 이동 async function b08Page() { let completeFail = false; const m = load("B08_Quantity/B08_Quantity_UI_Page.ts", { ...common, "./B08_Quantity_UI_Options": { createOptionsPanel: () => ({ panel: new Node(), save: async () => true }), }, "./B08_Quantity_UI_Hydro": { renderHydro() {}, request: async (url, init) => { calls.push(`req:${init?.method}:${url}`); if (completeFail) throw new Error("500"); }, }, }); m.renderB08Quantity(Object.assign(new Node(), { isRoot: true })); const step = async (i) => { take(); layoutOpts.onStepClick(i); await flush(); return take(); }; const out = { next: await step(6), back: await step(4) }; completeFail = true; out.fail = await step(6); return out; } // B09 [초기화] · [>](공용 틀 대신 셸) — draft = DRAFT.next 있음 / 없음 async function b09(withNext) { let failOn = null; // "PUT" | "complete" global.fetch = async (url, init) => { const method = init?.method ?? "GET"; if (method !== "GET") calls.push(`fetch:${method}:${url}`); const bad = failOn === method || (failOn === "complete" && url.endsWith("/complete")); if (bad) return { ok: false, status: 500, json: async () => ({}) }; return { ok: true, status: 200, json: async () => any() }; }; const tabs = any(); tabs.DRAFT = { builders: {}, extend: [], panels: [], actions: [] }; if (withNext) tabs.DRAFT.next = () => calls.push("next"); const nextBtn = Object.assign(new Node("button"), { disabled: true }); const m = load("B09_Estimation/B09_Estimation_UI_Shell.ts", { ...common, "./B09_Estimation_UI_Tabs": tabs, "./B09_Estimation_UI_Dock": { linkMode: () => "popup", createDock: (list) => { const dock = new Node(); dock.append(...list); return dock; }, }, "@ui/ui_template_workflow_layout": { createWorkflowLayout: (opts) => { layoutOpts = opts; // 공용 [?] [<] [>] 자리 흉내 — B09 [>] = 흐림 opts.leftPanel.querySelector = (sel) => (sel === '[data-step-nav="1"]' ? nextBtn : null); const root = new Node(); root.append(opts.leftPanel, opts.mainContent); return { root }; }, }, }); buttons.length = 0; m.renderB09Estimation(Object.assign(new Node(), { isRoot: true })); await flush(); // GET 응답 반영 const left = layoutOpts.leftPanel; const dock = left.children[left.children.length - 1]; const out = { dock: dock.children.map((b) => b.variant), nextEnabled: !nextBtn.disabled }; const nav = async (detail) => { take(); let stopped = false; const event = { detail, stopPropagation: () => (stopped = true) }; (left.listeners["ui-step-nav"] ?? []).forEach((fn) => fn(event)); await flush(); return [stopped, take()]; }; if (!withNext) { out.nav = await nav(1); return out; } out.clean = await nav(1); store.set("b09-estimation", { x: 1 }); out.changed = await nav(1); out.cleared = !store.has("b09-estimation"); store.set("b09-estimation", { x: 1 }); failOn = "PUT"; out.saveFail = await nav(1); failOn = "complete"; store.delete("b09-estimation"); out.completeFail = await nav(1); failOn = null; out.prev = await nav(-1); // [초기화] const reset = dock.children[0]; const press = async () => { take(); reset.onClick(); await flush(); return take(); }; store.set("b09-estimation", { x: 1 }); confirmAnswer = false; out.resetCancel = [await press(), store.has("b09-estimation")]; confirmAnswer = true; out.resetOk = [await press(), store.has("b09-estimation")]; store.clear(); return out; } (async () => { const r = { options: await b08Options(), page: await b08Page(), b09: await b09(true), b09NoDraft: await b09(false), }; console.log(JSON.stringify(r)); })().catch((e) => { console.error(e && e.stack ? e.stack : e); process.exit(1); });