- B08 줄 [?][<][>][저장][초기화] · POST quantity/reset · 초안 넷 비움 · 이정 저장본 다시 읽음 - B08 B09 쪽 이동 = 저장 뒤 POST quantity/complete · 실패면 머묾 - B09 줄 [?][<][>][초기화] · [내역 초기화] → [초기화] 하나(POST estimation/reset) - B09 [>] = 고친 값 저장 → POST estimation/complete → DRAFT.next(인쇄 미리보기) · 초안 없으면 흐림 - 시험: 새 test_298_2_b08_b09_buttons · test_184_3 · test_304 · test_481_2 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
255 lines
8.5 KiB
JavaScript
255 lines
8.5 KiB
JavaScript
/* 481-2 B08 · B09 단계 이동 = 저장 성공 뒤 — 진짜 `B08_Quantity_UI_Page.ts` · `B09_Estimation_UI_Shell.ts` 를
|
|
* 트랜스파일해 가짜 DOM · 가짜 의존 위에서 돌리고 값을 JSON 으로 냄.
|
|
* 의존은 기록용 가짜 — 지정 안 한 import 는 무엇이든 받아 주는 빈 값(any). */
|
|
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(),
|
|
});
|
|
}
|
|
|
|
// 가짜 요소 — isConnected = 뿌리까지 부모 줄이 이어짐
|
|
class Node {
|
|
constructor(tag = "div") {
|
|
this.tagName = tag.toUpperCase();
|
|
this.children = [];
|
|
this.parent = null;
|
|
this.dataset = {};
|
|
this.style = {};
|
|
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() {}
|
|
}
|
|
global.document = { createElement: (tag) => new Node(tag) };
|
|
|
|
function load(rel, stubs) {
|
|
let 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 = () => new Promise((r) => setTimeout(r, 0));
|
|
|
|
const calls = [];
|
|
let layoutOpts = null;
|
|
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) => Object.assign(new Node("button"), { onClick: o.onClick }),
|
|
showToast: (text, kind) => calls.push(`toast:${kind}`),
|
|
showLoadingOverlay() {},
|
|
hideLoadingOverlay() {},
|
|
showConfirmDialog: async () => false,
|
|
},
|
|
"@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"],
|
|
goToWorkflowStage: (_p, route) => calls.push(`go:${route}`),
|
|
},
|
|
"../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" };
|
|
|
|
async function b08() {
|
|
let optionsOk = true;
|
|
let tabOk = true;
|
|
let registerMaterial = null;
|
|
let registerHydro = null;
|
|
const m = load("B08_Quantity/B08_Quantity_UI_Page.ts", {
|
|
...common,
|
|
"./B08_Quantity_UI_Options": {
|
|
createOptionsPanel: () => ({
|
|
panel: new Node(),
|
|
save: async () => (calls.push("save:options"), optionsOk),
|
|
}),
|
|
},
|
|
"./B08_Quantity_UI_Material": {
|
|
renderMaterialSheet: (_h, _p, _s, onSave) => (registerMaterial = onSave),
|
|
},
|
|
"./B08_Quantity_UI_Hydro": {
|
|
renderHydro: (_h, _p, onSave) => (registerHydro = onSave),
|
|
request: async (url) => calls.push(`req:${url.split("/").pop()}`), // 298-2 B09 쪽 = 단계 5 완료
|
|
},
|
|
});
|
|
const root = Object.assign(new Node(), { isRoot: true });
|
|
m.renderB08Quantity(root);
|
|
const body = layoutOpts.mainContent;
|
|
const tabBar = body.children[0];
|
|
const clickTab = (id) => tabBar.children.find((b) => b.dataset.b08Tab === id).onClick();
|
|
const step = async (i) => {
|
|
calls.length = 0;
|
|
layoutOpts.onStepClick(i);
|
|
await flush();
|
|
await flush();
|
|
return [...calls];
|
|
};
|
|
const tabSave = (tag) => async () => (calls.push(`save:${tag}`), tabOk);
|
|
const out = {};
|
|
out.summaryOk = await step(6);
|
|
optionsOk = false;
|
|
out.optionsFail = await step(6);
|
|
optionsOk = true;
|
|
clickTab("material");
|
|
registerMaterial(tabSave("material"));
|
|
out.materialOk = await step(4);
|
|
tabOk = false;
|
|
out.materialFail = await step(4);
|
|
optionsOk = false;
|
|
out.optionsFailSkipsTab = await step(4);
|
|
optionsOk = true;
|
|
tabOk = false;
|
|
clickTab("hydrology");
|
|
registerHydro(tabSave("hydro"));
|
|
out.hydroFail = await step(6);
|
|
clickTab("earthwork"); // 수리 탭 떠남 — 떨어진 탭 저장은 안 부름
|
|
out.leftTab = await step(6);
|
|
return out;
|
|
}
|
|
|
|
async function b09() {
|
|
let fail = false;
|
|
global.fetch = async (url, init) => {
|
|
calls.push(`fetch:${init?.method ?? "GET"}:${url}`);
|
|
if (fail && init?.method === "PUT") return { ok: false, status: 500, json: async () => ({}) };
|
|
return { ok: true, status: 200, json: async () => any(), text: async () => "{}" };
|
|
};
|
|
const m = load("B09_Estimation/B09_Estimation_UI_Shell.ts", common);
|
|
calls.length = 0;
|
|
m.renderB09Estimation(Object.assign(new Node(), { isRoot: true }));
|
|
const step = async (i) => {
|
|
calls.length = 0;
|
|
layoutOpts.onStepClick(i);
|
|
for (let k = 0; k < 5; k++) await flush();
|
|
return calls.filter((c) => !c.startsWith("toast"));
|
|
};
|
|
const out = {};
|
|
out.beforeLoad = await (async () => {
|
|
store.set("b09-estimation", { x: 1 });
|
|
return step(5); // 불러오기 전 — 저장 없이 이동 · 캐시는 남음
|
|
})();
|
|
for (let k = 0; k < 5; k++) await flush(); // GET 응답 반영
|
|
store.delete("b09-estimation");
|
|
out.clean = await step(5);
|
|
store.set("b09-estimation", { x: 1 });
|
|
fail = true;
|
|
out.fail = await step(5);
|
|
fail = false;
|
|
out.ok = await step(5);
|
|
return out;
|
|
}
|
|
|
|
// 진짜 좌측 옵션 `save` — 캐시 없으면 요청 없이 참 · 1본 빈칸 = 거짓 · 요청 실패 = 거짓
|
|
async function options() {
|
|
let fail = false;
|
|
const m = load("B08_Quantity/B08_Quantity_UI_Options.ts", {
|
|
...common,
|
|
"@ui/ui_template_elements": {
|
|
...common["@ui/ui_template_elements"],
|
|
createInputField: () => ({ input: new Node("input"), root: new Node() }),
|
|
},
|
|
"@ui/ui_template_collapsible": { attachCollapsible() {}, createPanelCard: () => new Node() },
|
|
"./B08_Quantity_UI_Hydro": {
|
|
request: async (url, init) => {
|
|
calls.push(`req:${init?.method ?? "GET"}`);
|
|
if (fail && init?.method === "PUT") 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"),
|
|
},
|
|
});
|
|
const panel = m.createOptionsPanel("P1", () => calls.push("redraw"));
|
|
await flush();
|
|
const run = async () => {
|
|
calls.length = 0;
|
|
const ok = await panel.save();
|
|
return [ok, calls.filter((c) => !c.startsWith("toast"))];
|
|
};
|
|
const out = {};
|
|
store.clear();
|
|
out.clean = await run();
|
|
store.set("b08-options", { soft_rock_pct: 80 });
|
|
out.changed = await run();
|
|
out.after = store.has("b08-options");
|
|
store.set("b08-milestone", { x: 1 });
|
|
out.milestoneOnly = await run();
|
|
store.set("b08-options", { one_pipe_length_m: null });
|
|
out.blankPipe = await run();
|
|
store.set("b08-options", { soft_rock_pct: 70 });
|
|
fail = true;
|
|
out.fail = await run();
|
|
out.keptOnFail = store.has("b08-options");
|
|
store.clear();
|
|
return out;
|
|
}
|
|
|
|
(async () => {
|
|
const r = { b08: await b08(), b09: await b09(), options: await options() };
|
|
console.log(JSON.stringify(r));
|
|
})().catch((e) => {
|
|
console.error(e && e.stack ? e.stack : e);
|
|
process.exit(1);
|
|
});
|