- ui_template setLoadingOverlayProgress(ratio, label) — 잠금판 스피너 자리에 진행 원 · 참조가 모두 풀리면 함께 걷힘
- b_workflow_nav moveStage(projectId, 주소 또는 이동 함수, steps) — 단계 { 이름 · 실행 · 선택 서버 진행 묻기 } · 거짓 · 던짐 = 잠금 풀고 머묾(던짐은 까닭 토스트) · 넘기는 중 다시 누름 무시
- goToWorkflowStage 가 Promise 를 돌려줌 — 잠금이 이동 뒤에 풀림
- 낡은 시험 고침 — 298-2 · 481-2 가짜 의존에 moveStage · 290-1 · 280-5 글 약속을 새 길로 · 520-3 시험 새로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
307 lines
10 KiB
JavaScript
307 lines
10 KiB
JavaScript
/* 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);
|
|
});
|