/* 153-1 B07 횡단 장 배치 칸 헬퍼 — 좌측 「횡단도」 묶음 맨 아래 자리 · 공용 입력칸 · 작은 단추를 * Node 로 돌려 표준출력 JSON 한 벌로. TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 · * 공용 단추 · 입력칸은 진짜(`ui_template_elements`) · 알림만 기록용 가짜. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const B07 = path.join(ROOT, "B07_DesignDetail"); const UI = path.join(ROOT, "ui_template"); const cache = new Map(); class FakeNode { constructor(tag) { this.tagName = tag.toUpperCase(); this.children = []; this.dataset = {}; this.attrs = {}; this.listeners = {}; this._text = ""; this._classes = new Set(); this.value = ""; this.disabled = false; const self = this; this.classList = { add: (...names) => names.forEach((n) => self._classes.add(n)), remove: (...names) => names.forEach((n) => self._classes.delete(n)), contains: (name) => self._classes.has(name), }; } set className(value) { this._classes = new Set(String(value).split(/\s+/).filter(Boolean)); } get className() { return [...this._classes].join(" "); } set textContent(value) { this._text = String(value); this.children = []; } get textContent() { return this._text + this.children.map((c) => c.textContent).join(""); } setAttribute(key, value) { this.attrs[key] = String(value); } append(...kids) { kids.forEach((k) => this.children.push(k)); } replaceChildren(...kids) { this.children = [...kids]; } addEventListener(type, handler) { (this.listeners[type] ||= []).push(handler); } click() { if (!this.disabled) (this.listeners.click || []).forEach((handler) => handler({})); } } const all = (node) => [node, ...node.children.flatMap(all)]; const byClass = (node, name) => all(node).filter((n) => n.classList.contains(name)); globalThis.document = { createElement: (tag) => new FakeNode(tag) }; const toasts = []; function loadTs(file) { const full = path.resolve(file); if (cache.has(full)) return cache.get(full); const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; if (request === "@ui/ui_template_elements") return { ...loadTs(path.join(UI, "ui_template_elements.ts")), showToast: (message, kind) => toasts.push([kind, message]), }; // 공용 카드(269) = 진짜 모듈 if (request === "@ui/ui_template_collapsible") return loadTs(path.join(UI, "ui_template_collapsible.ts")); if (request === "@ui/ui_template_locale") return { currentLanguageIndex: 0, ui_locales: new Proxy({}, { get: (_, key) => [String(key)] }), }; if (request.startsWith(".")) { const target = path.join(path.dirname(full), `${request}.ts`); return fs.existsSync(target) ? loadTs(target) : {}; } return {}; // 이 시험이 안 부르는 바깥 길(API · 설정 · CAD) }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const { buildDrawingSidePanel, buildCrossLayoutControls } = loadTs( path.join(B07, "B07_DesignDetail_UI_Panels.ts"), ); const out = {}; const slot = new FakeNode("div"); slot.className = "b07-cross-layout-slot"; const panel = buildDrawingSidePanel( [ { id: "cross_standard", kind: "cross_standard", label: "표준 횡단면도", confirmed: false }, { id: "cross_s1", kind: "cross", label: "1장", confirmed: false }, { id: "cross_s2", kind: "cross", label: "2장", confirmed: true }, ], () => {}, undefined, false, undefined, slot, ); const cross = all(panel).find((n) => n.dataset.kind === "cross"); out.slotLast = cross.children.at(-1) === slot; out.slotCount = all(panel).filter((n) => n === slot).length; // 장 — [이 장] 켜짐 · 값 바꿔 누름 const applied = []; const defaults = []; slot.replaceChildren( buildCrossLayoutControls( [3, 4], (grid) => applied.push(grid), (grid) => defaults.push(grid), ), ); const box = slot.children[0]; const buttons = () => all(slot).filter((n) => n.tagName === "BUTTON"); const inputs = () => all(slot).filter((n) => n.tagName === "INPUT"); out.sheet = { boxClass: box.className, labels: byClass(slot, "ui-field__label").map((n) => n.textContent), inputs: inputs().map((n) => ({ value: n.value, type: n.attrs.type, min: n.attrs.min, max: n.attrs.max, cls: n.className, })), buttons: buttons().map((n) => ({ text: n.textContent, cls: n.className, disabled: n.disabled })), }; inputs()[0].value = "2"; inputs()[1].value = "5"; buttons()[0].click(); buttons()[1].click(); inputs()[0].value = "11"; buttons()[0].click(); out.sheetClicks = { applied, defaults, toasts: [...toasts] }; // 장 아님 — [이 장] 꺼짐 · [모든 장]은 됨 const defaults2 = []; slot.replaceChildren(buildCrossLayoutControls([4, 4], null, (grid) => defaults2.push(grid))); buttons()[0].click(); buttons()[1].click(); out.notSheet = { disabled: buttons().map((n) => n.disabled), defaults: defaults2, values: inputs().map((n) => n.value), }; // 180-2 표 배율 칸 — 배치 칸 줄 뒤 · 0.5~2.0 · step 0.1 · [배율 적용] const scales = []; const toastStart = toasts.length; slot.replaceChildren( buildCrossLayoutControls([4, 4], null, () => {}, { scale: { value: 1, onApply: (value) => scales.push(value) }, }), ); const scaleInput = inputs()[2]; out.scale = { labels: byClass(slot, "ui-field__label").map((n) => n.textContent), input: { value: scaleInput.value, min: scaleInput.attrs.min, max: scaleInput.attrs.max, step: scaleInput.attrs.step, }, buttons: buttons().map((n) => n.textContent), }; scaleInput.value = "1.5"; buttons()[2].click(); scaleInput.value = "2.5"; buttons()[2].click(); out.scale.applied = scales; out.scale.toasts = toasts.slice(toastStart).map((t) => t[0]); process.stdout.write(JSON.stringify(out));