/* 311 좌측 패널 기본 접힘 헬퍼 — B05 「계획노선」 · B06 「횡단 보기 설정」(한 벌 패널 `design_panel`)과 * B07 도면 목록(평면도 · 종단도 · 횡단도)을 가짜 DOM 위에 세워 첫 그림 접힘 · 편 뒤 다시 그림 유지를 본다. * 결과는 JSON 한 벌. 꼴은 `helper_design_view_parity.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; } } // 공용 attachCollapsible 이 쓰는 closest(`.클래스` 하나) FakeNode.prototype.closest = function (selector) { for (let n = this; n; n = n.parent) if (n.classList && n.classList.contains(selector.slice(1))) return n; return null; }; globalThis.document = { createElement: (tag) => new FakeNode(tag) }; /** 제목 누름 — 패널 뿌리에 걸린 공용 위임 손잡이로 올려 보냄(가짜 DOM 은 거품이 없음). */ const press = (root, section) => { const target = section.querySelector(".ui-collapsible__title"); (root.listeners.click || []).forEach((h) => h({ target })); }; const node = (tag = "div") => new FakeNode(tag); const STUBS = { "@ui/ui_template_locale": { ui_locales: new Proxy({}, { get: (_, k) => [String(k)] }), currentLanguageIndex: 0, }, "@ui/ui_template_elements": { createButton: ({ label }) => Object.assign(node("button"), { _text: label }), createInputField: () => ({ root: node(), input: node("input") }), createSelectField: () => ({ root: node(), select: { value: "" } }), showToast: () => undefined, }, "@ui/ui_template_checkbox": { createCheckbox: () => ({ root: node("label"), input: {} }) }, "../B05_Profile/B05_Profile_UI_Structures_Fields": { stationFields: () => ({ wrap: node() }) }, "./B07_DesignDetail_UI_PlanManage": { openPlanManager: () => undefined }, "./B07_DesignDetail_Api_Fetch": {}, "./B06_Section_UI_Page_Common": { L: (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 === "@ui/ui_template_collapsible") return loadTs(path.join(ROOT, "ui_template", "ui_template_collapsible.ts")); 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 out = {}; const titleOf = (n) => n.children.find((c) => c.tagName === "h3")?.textContent ?? ""; const folds = (root) => Object.fromEntries( root .querySelectorAll(".ui-collapsible") .map((s) => [titleOf(s), s.classList.contains("is-collapsed")]), ); // ── B05 · B06 한 벌 패널 ─────────────────────────────────────────────────────── const design = loadTs(path.join(ROOT, "A00_Common", "design", "design_panel.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")); const text = (t) => Object.assign(node(), { _text: t }); const run = { run: () => undefined }; const panel = design.createDesignPanel( { projectId: "p", parts: { structures: { root: node("section"), listRoot: node() }, settings: { nodes: [text("a")] }, recommend: { nodes: [text("rec")] }, crossView: { row: text("cross-row") }, standardSlot: text("std"), }, dock: { reset: run, save: run, confirm: run, switchView: run }, }, "profile", ); const byTitle = (title) => panel.root.querySelectorAll(".ui-collapsible").find((s) => titleOf(s) === title); out.profileFirst = folds(panel.root); const route = byTitle("페이지 설정"); // 345-3 계획노선 칸 지움 — 접힌 채 서는 칸 press(panel.root, route); // 사용자가 폄(공용 attachCollapsible) panel.setView("cross"); out.crossFirst = folds(panel.root); press(panel.root, byTitle("B06_View_Title")); panel.setView("profile"); panel.setView("cross"); out.afterRedraw = folds(panel.root); out.sameRouteNode = byTitle("페이지 설정") === route; // ── B07 도면 목록 ───────────────────────────────────────────────────────────── const b07 = loadTs(path.join(ROOT, "B07_DesignDetail", "B07_DesignDetail_UI_Panels.ts")); const d = (id, kind) => ({ id, kind, label: id, confirmed: false }); const drawings = [ d("longitudinal", "longitudinal"), d("longitudinal_2", "longitudinal"), d("cross_1", "cross"), d("cross_2", "cross"), d("plan_a", "plan"), d("cover", "cover"), d("cross_standard", "cross_standard"), d("mass_haul", "mass_haul"), d("mass_haul_2", "mass_haul"), ]; const groups = (p) => Object.fromEntries( p .querySelectorAll(".b07-drawing-group") .map((s) => [s.dataset.kind, s.classList.contains("is-collapsed")]), ); const fresh = node(); out.b07EntryFolded = [...b07.foldedGroups(fresh)]; const first = b07.buildDrawingSidePanel( drawings, () => undefined, undefined, false, undefined, undefined, b07.foldedGroups(fresh), ); out.b07First = groups(first); const host = node(); host.append(first); first .querySelectorAll(".b07-drawing-group") .find((s) => s.dataset.kind === "cross") .classList.remove("is-collapsed"); first .querySelectorAll(".b07-drawing-group") .find((s) => s.dataset.kind === "mass_haul") .classList.add("is-collapsed"); const again = b07.buildDrawingSidePanel( drawings, () => undefined, undefined, false, undefined, undefined, b07.foldedGroups(host), ); out.b07Redraw = groups(again); process.stdout.write(JSON.stringify(out));