Files
Aislo/resources/tester/helper_481_2_save_move.cjs
T
eomsangdonandClaude Opus 5.5 59a694a7ed feat(B09): 단가연결 즉시 금액(뒤에서 서버 셈) · [저장] 단추 · 옮길 때 물음(PLAN 702-3)
- 새 산근(로직 연결 · 입력값) = 연결 즉시 뒤에서 POST …/estimation/calc(화면 캐시로 셈 ·
  저장 안 함) · 차례 번호 · 옛 요청 끊기 · 오는 동안 그 줄 금액 칸만 「셈 중」 · 잠금 없음
  (새 `B09_Estimation_UI_Live.ts`) · 기준 = payload.user(서버가 그 셈에 쓴 값)
- 조작 중 셈 = 마지막 GET · [저장] 의 B08 원천 다시 씀(120 s · 0.6 → 0.15 s) · [저장] · GET 은 늘 새로 읽음
- [저장] 단추 = 왼쪽 아래 [초기화] 옆(B08 과 같은 부품 · 꼴) · 고침 없으면 알림만
- 단계 이동 · [>] = 저장 안 한 고침 있으면 공용 떠남 물음(취소 · 폐기 · 저장) — 옛 자동 저장 없앰
- 서고 사본 payload 의 user 받음(서버 기준) — 화면 캐시와 다른 로직 줄은 뒤에서 다시 셈
- 낡은 시험 고침: 481-2 · 298-2(자동 저장 → 물음 · [저장] 단추) · 184-3 · 304(단추 줄) ·
  204-2(user 받음) · 200-2(701-3 띠 · 702-1 펼침 뒤 셸 꼴)
- 확인 = test_702_3 3 · B09 관련 312 통과 · typecheck 0 · 검증_sub5 T6 11 누름 · 연결 뒤 0.32~0.49 s 금액

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:11:44 +09:00

304 lines
10 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(); // 가짜 세션 캐시 — 이름 → 값
let leaveGuard = null;
let leaveAnswer = "save"; // save | discard | stay
const common = {
"../A00_Common/b_page_tabs": {
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
fillTabs: (host, o) => {
const kids = o.tabs.map((t) =>
Object.assign(new Node("button"), {
onClick: () => t.key !== o.value && o.onChange(t.key),
dataset: { [o.data]: t.key },
}),
);
host.replaceChildren(...kids);
return kids;
},
},
"@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"],
// 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;
},
},
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4 · 702-3 B09 단계 이동 물음 = 가드 흉내(leaveAnswer)
"../A00_Common/b_project_switch": {
currentProjectId: () => "P1",
guardProjectLeave: (_root, g) => (leaveGuard = g),
askProjectLeave: async () => {
if (!leaveGuard || !leaveGuard.dirty()) return true;
calls.push("ask");
if (leaveAnswer === "stay") return false;
if (leaveAnswer === "discard") return (await leaveGuard.discard?.(), true);
return (await leaveGuard.save()) !== false;
},
},
"../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);
// 702-3 물음 [버림] = 캐시 지우고 이동 · [머묾] = 요청 없이 머묾
store.set("b09-estimation", { x: 1 });
leaveAnswer = "discard";
out.discard = [await step(5), store.has("b09-estimation")];
store.set("b09-estimation", { x: 1 });
leaveAnswer = "stay";
out.stay = [await step(5), store.has("b09-estimation")];
leaveAnswer = "save";
store.delete("b09-estimation");
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);
});