/* 두 보기 좌측 패널 **한 벌 지킴** 헬퍼(계획서 24-2f · 24-3b) — 공용 등록부(`design_panel` · `design_dock` · * `design_sections`)와 두 보기 칸 정의를 트랜스파일해 가짜 DOM 위에 **패널 하나**를 세우고 * `setView` 로 보기를 오가며 같은 노드인지 본다(한 벌 `design_shared` 가 하는 그대로). 결과는 JSON 한 벌. * * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); // ── 화면 그릇 흉내 — 요소 · 클래스 · 속성 · 글자 · 자식 ────────────────────────── class FakeNode { constructor(tag) { this.tagName = tag; this.children = []; this.parent = null; this.dataset = {}; this.style = {}; this.attrs = {}; this._text = ""; this._classes = new Set(); this.hidden = false; this.disabled = false; this.title = ""; this.listeners = {}; const self = this; this.classList = { add: (...n) => n.forEach((x) => self._classes.add(x)), remove: (...n) => n.forEach((x) => self._classes.delete(x)), toggle: (n, f) => { const on = f === undefined ? !self._classes.has(n) : !!f; on ? self._classes.add(n) : self._classes.delete(n); return on; }, contains: (n) => self._classes.has(n), }; } set className(v) { this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); } get className() { return [...this._classes].join(" "); } set textContent(v) { this._text = String(v); this.children.forEach((c) => (c.parent = null)); this.children = []; } get textContent() { return this._text + this.children.map((c) => c.textContent).join(""); } _adopt(kid) { const node = typeof kid === "string" ? Object.assign(new FakeNode("#text"), { _text: kid }) : kid; if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node); node.parent = this; return node; } append(...kids) { kids.forEach((k) => this.children.push(this._adopt(k))); } prepend(...kids) { this.children.unshift(...kids.map((k) => this._adopt(k))); } insertBefore(node, ref) { const adopted = this._adopt(node); const at = this.children.indexOf(ref); if (at < 0) this.children.push(adopted); else this.children.splice(at, 0, adopted); } replaceChildren(...kids) { this.children = []; this.append(...kids); } setAttribute(k, v) { this.attrs[k] = String(v); } removeAttribute(k) { delete this.attrs[k]; if (k === "title") this.title = ""; } addEventListener(type, handler) { (this.listeners[type] ||= []).push(handler); } click() { if (this.disabled) return; (this.listeners.click || []).forEach((h) => h({ target: this })); } querySelector(selector) { return this.querySelectorAll(selector)[0] ?? null; } querySelectorAll(selector) { const cls = selector.slice(1); const out = []; const walk = (n) => n.children.forEach((k) => { if (k.classList && k.classList.contains(cls)) out.push(k); walk(k); }); walk(this); return out; } } globalThis.document = { createElement: (tag) => new FakeNode(tag) }; function html(node) { if (node.tagName === "#text") return node._text; const cls = node.className ? ` class="${node.className}"` : ""; const attrs = Object.entries(node.attrs) .sort() .map(([k, v]) => ` ${k}="${v}"`) .join(""); const hidden = node.hidden ? " hidden" : ""; const off = node.disabled ? " disabled" : ""; const inner = node._text + node.children.map(html).join(""); return `<${node.tagName}${cls}${attrs}${hidden}${off}>${inner}`; } // ── 바깥 모듈 대역 ───────────────────────────────────────────────────────────── const LOCALES = { Common_Btn_Reset: ["초기화", "Reset"], B06_Profile_Btn_Save: ["저장", "Save"], B06_Profile_Btn_Confirm: ["확정", "Confirm"], Design_Btn_ToCross: ["횡단 보기", "Cross view"], Design_Btn_ToProfile: ["종단 보기", "Profile view"], }; const STUBS = { "@ui/ui_template_locale": { ui_locales: LOCALES, currentLanguageIndex: 0 }, "@ui/ui_template_collapsible": { attachCollapsible: () => undefined }, "@ui/ui_template_elements": { createButton: ({ label, variant, onClick }) => { const button = new FakeNode("button"); button.className = `ui-btn ui-btn--${variant ?? "ghost"}`; const span = new FakeNode("span"); span.className = "ui-btn__label"; span.textContent = label; button.append(span); button.addEventListener("click", () => onClick && onClick()); return button; }, createInputField: () => ({ root: new FakeNode("div"), input: new FakeNode("input") }), }, "./B06_Section_UI_Page_Common": { L: (key) => ({ B06_View_Title: "횡단 보기 설정", B06_Std_Title: "표준 횡단면 설정" })[key] ?? key, }, }; 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 (STUBS[request]) return STUBS[request]; if (request.endsWith(".css")) return {}; if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); return require(request); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const panel = loadTs(path.join(ROOT, "A00_Common", "design", "design_panel.ts")); loadTs(path.join(ROOT, "A00_Common", "design", "design_sections.ts")); loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Panel_Sections.ts")); loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Panel.ts")); // ── 견본 — 두 보기 부품을 한 `parts` 에 모은다(한 벌 `design_shared` 와 같은 모양) ────────── function sample() { const root = new FakeNode("section"); root.className = "b05-route__panel-section b05-structure-section ui-collapsible ui-sidebar-section"; const title = new FakeNode("h3"); title.className = "ui-collapsible__title"; title.textContent = "구조물 배치"; root.append(title); const listRoot = new FakeNode("div"); listRoot.className = "b05-structure__list"; listRoot.textContent = "구조물 목록"; return { root, listRoot }; } const text = (t) => Object.assign(new FakeNode("div"), { _text: t }); const calls = []; let current = "profile"; const run = (name) => () => calls.push(`${current}:${name}`); const built = panel.createDesignPanel( { projectId: "p", parts: { structures: sample(), settings: { nodes: [text("a"), text("b")] }, route: { plannedRouteButton: text("plan"), calcNodes: [text("calc")], selectedNodes: [text("sel")], bindSelectedRoot: (root) => { root.hidden = true; }, }, crossView: { row: text("cross-row") }, standardSlot: text("std"), }, dock: { reset: { run: run("reset") }, save: { run: run("save") }, confirm: { run: run("confirm") }, switchView: { run: run("switch") }, }, }, "profile", ); const titleOf = (node) => node.children.find((c) => c.tagName === "h3")?.textContent ?? ""; const nodes = () => { const dockRoot = built.dock.root; return { root: built.root, dock: dockRoot, buttons: dockRoot.querySelectorAll(".ui-btn"), structures: built.root.children.find((c) => c.classList.contains("b05-structure-section")), }; }; function snapshot() { const { dock, buttons, structures } = nodes(); const shown = built.root.children.filter((c) => c !== dock && c.style.display !== "none"); buttons.forEach((b) => b.click()); return { rootClass: built.root.className, shownTitles: shown.map(titleOf), structuresFirst: shown[0] === structures, structuresHtml: html(structures), dockFirstChild: html(dock.children[0]), dockIsLast: built.root.children[built.root.children.length - 1] === dock, dockLabels: buttons.map((b) => b.textContent), dockActions: buttons.map((b) => b.dataset.dockAction), dockDisabled: buttons.map((b) => b.disabled), }; } const first = nodes(); const views = { profile: snapshot() }; current = "cross"; built.setView("cross"); views.cross = snapshot(); const afterCross = nodes(); current = "profile"; built.setView("profile"); views.back = snapshot(); const afterBack = nodes(); const same = (a, b) => a.root === b.root && a.dock === b.dock && a.structures === b.structures && a.buttons.length === b.buttons.length && a.buttons.every((button, index) => button === b.buttons[index]); process.stdout.write( JSON.stringify({ views, calls, sameNodes: { cross: same(first, afterCross), back: same(first, afterBack) }, childCount: built.root.children.length, sectionIds: { profile: panel.panelSectionsFor("profile").map((d) => d.id), cross: panel.panelSectionsFor("cross").map((d) => d.id), }, }), );