/* 153-1 · 523-1 B07 횡단 장 배치 헬퍼 — 좌측 「횡단도」 카드 ⚙ 하나 · ⚙ 횡단 모달(그룹 상자 * 「장 배치」 · 「표」 · 맨 아래 단추 줄 하나 · [확인] = 바뀐 것만 저장)을 Node 로 돌려 표준출력 JSON 한 벌로. * TS 는 그 자리에서 트랜스파일 · 화면 그릇(DOM)은 쓰는 만큼만 흉내 · 공용 단추 · 입력칸 · 고르기칸은 진짜 * (`ui_template_elements`) · 모달 겉틀 · 알림만 기록용 가짜. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); 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"); 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() { const event = { stopPropagation() {}, preventDefault() {} }; if (!this.disabled) (this.listeners.click || []).forEach((handler) => handler(event)); } } 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 = []; const modals = []; const openModal = (opts) => { const body = new FakeNode("div"); const modal = { opts, body, open: true }; modals.push(modal); opts.mount(body, () => (modal.open = false), new FakeNode("div")); return { close: () => (modal.open = false) }; }; const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (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]), }; if (request === "@ui/ui_template_modal") return { openModal }; if (request === "./B07_DesignDetail_UI_PlanManage") return {}; if (request === "./B07_DesignDetail_Api_Fetch") return {}; // 공용 카드(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) }, }); const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Panels.ts")); const { openDrawingSettings } = loadTs(path.join(B07, "B07_DesignDetail_UI_Settings.ts")); const out = {}; const settings = []; const drawings = [ { 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 }, ]; const panel = buildDrawingSidePanel( drawings, () => {}, undefined, false, (kind) => settings.push(kind), ); // 280-5 배치 칸은 ⚙ 횡단 모달로 — 카드엔 ⚙ 하나 · 배치 칸 없음 const cross = all(panel).find((n) => n.dataset.kind === "cross"); const gears = byClass(cross, "ui-collapsible__settings"); out.card = { gears: gears.length, layoutInCard: byClass(cross, "b07-cross-layout").length, }; gears[0].click(); out.card.settings = settings; // ── ⚙ 횡단 모달(523-1) ───────────────────────────────────────────────────── const tick = () => new Promise((r) => setTimeout(r, 0)); const saved = []; let current = drawings[1]; // cross_s1 = 1장 const guideToggle = new FakeNode("button"); guideToggle.textContent = "안내선 숨기기"; const deps = { drawings, current: () => current, canRedraw: () => false, redraw: async () => false, cross: { layout: async () => ({ default: [4, 4], sheets: { 1: [3, 4] }, table_scale: 1 }), save: (next, sheet) => saved.push([next, sheet]), guideToggle, }, }; const open = async () => { openDrawingSettings("cross", deps); await tick(); const modal = modals.at(-1); const inputs = all(modal.body).filter((n) => n.tagName === "INPUT"); const select = all(modal.body).find((n) => n.tagName === "SELECT"); const footer = byClass(modal.body, "b07-settings__actions")[0]; const btn = (label) => all(footer).find((n) => n.textContent === label); const type = (input, value) => { input.value = value; (input.listeners.input || []).forEach((h) => h()); }; return { modal, inputs, select, footer, btn, type }; }; (async () => { // 장 — 그룹 상자 둘 · 공용 칸 · 단추 줄 하나 let m = await open(); const legends = byClass(m.modal.body, "b07-settings__group").map( (g) => g.children[0].textContent, ); const bodyButtons = all(m.modal.body).filter((n) => n.tagName === "BUTTON"); out.sheet = { legends, labels: byClass(m.modal.body, "ui-field__label").map((n) => n.textContent), inputs: m.inputs.map((n) => ({ value: n.value, type: n.attrs.type, min: n.attrs.min, max: n.attrs.max, step: n.attrs.step ?? null, cls: n.className, })), scope: { options: m.select.children.map((o) => o.textContent), value: m.select.value }, footers: byClass(m.modal.body, "b07-settings__actions").length, footerButtons: m.footer.children.map((n) => n.textContent), buttonsOutsideFooter: bodyButtons.filter((n) => !all(m.footer).includes(n)).length, }; // 이 장만 — 2 × 5 m.type(m.inputs[0], "2"); m.type(m.inputs[1], "5"); m.btn("확인").click(); await tick(); out.applySheet = { saved: [...saved], closed: !m.modal.open }; // 모든 장 — 고르기만 바꿔도 씀(장마다 값 지움) saved.length = 0; m = await open(); m.select.value = "all"; m.btn("확인").click(); await tick(); out.applyAll = [...saved]; // 범위 밖 = 칸 붉음 · [확인] 꺼짐 · 저장 없음 saved.length = 0; m = await open(); m.type(m.inputs[0], "11"); const confirm = m.btn("확인"); out.badGrid = { disabled: confirm.disabled, error: m.inputs[0].className.includes("ui-input--error"), }; confirm.click(); await tick(); out.badGrid.saved = saved.length; out.badGrid.open = m.modal.open; // 표 배율 — 1.5 = 배율만 · 2.5 = 꺼짐 m = await open(); m.type(m.inputs[2], "2.5"); out.badTable = m.btn("확인").disabled; m.type(m.inputs[2], "1.5"); m.btn("확인").click(); await tick(); out.applyTable = [...saved]; // 장 아님(표준 횡단면도) — 고르기 = 모든 장 하나 · 손대지 않고 [확인] = 저장 없이 닫음 saved.length = 0; current = drawings[0]; m = await open(); out.notSheet = { options: m.select.children.map((o) => o.textContent), values: m.inputs.map((n) => n.value), }; m.btn("확인").click(); await tick(); out.notSheet.saved = saved.length; out.notSheet.closed = !m.modal.open; // [취소] = 저장 없이 닫음 current = drawings[1]; m = await open(); m.type(m.inputs[0], "2"); m.btn("취소").click(); out.cancel = { saved: saved.length, closed: !m.modal.open }; process.stdout.write(JSON.stringify(out)); })();