/* PLAN 294-1 · 294-2 시험 헬퍼 — `B06_Section_UI_Adjust_Dock.ts` 를 트랜스파일해 작은 가짜 DOM 위에서 돌림. * 294-1 — 강조 켜고 끈 뒤에도 이름표 [삭제] 단추 · 원래 글 · 괄호 붙임도 글 칸만. * 294-2 — 물넘이포장(배관 명세 숨음)에서 side "pipe" = 「경사각도」 상자 강조. * 결과 = 표준출력 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, "..", ".."); // ── 작은 가짜 DOM — 태그 · 클래스 · 속성 · 자식 · 단순 선택자(쉼표 · `>` 하나) ────────── const TEXT = 3; class Text { constructor(data) { this.nodeType = TEXT; this.textContent = data; this.parentNode = null; } } class Comment { constructor() { this.nodeType = 8; this.parentNode = null; } } const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); class El { constructor(tag) { this.nodeType = 1; this.tagName = tag.toUpperCase(); this.attrs = {}; this.childNodes = []; this.parentNode = null; this.hidden = false; const self = this; this.dataset = new Proxy( {}, { get: (_, key) => self.attrs[`data-${kebab(String(key))}`], set: (_, key, value) => ((self.attrs[`data-${kebab(String(key))}`] = String(value)), true), deleteProperty: (_, key) => delete self.attrs[`data-${kebab(String(key))}`], }, ); this.classList = { set: new Set(), add: (...n) => n.forEach((c) => self.classList.set.add(c)), remove: (...n) => n.forEach((c) => self.classList.set.delete(c)), contains: (c) => self.classList.set.has(c), toggle: (c, on) => (on ?? !self.classList.set.has(c)) ? self.classList.add(c) : self.classList.remove(c), }; } set className(v) { this.classList.set = new Set(String(v).split(/\s+/).filter(Boolean)); } get children() { return this.childNodes.filter((n) => n instanceof El); } get firstChild() { return this.childNodes[0] ?? null; } get firstElementChild() { return this.children[0] ?? null; } get parentElement() { return this.parentNode instanceof El ? this.parentNode : null; } get textContent() { return this.childNodes.map((n) => (n.nodeType === 8 ? "" : n.textContent)).join(""); } set textContent(v) { this.childNodes.forEach((n) => (n.parentNode = null)); this.childNodes = []; this.append(String(v)); } setAttribute(k, v) { this.attrs[k] = String(v); } getAttribute(k) { return this.attrs[k] ?? null; } hasAttribute(k) { return k === "hidden" ? this.hidden : k in this.attrs; } append(...kids) { for (const k of kids) { const node = typeof k === "string" ? new Text(k) : k; node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1); node.parentNode = this; this.childNodes.push(node); } } after(node) { const list = this.parentNode.childNodes; node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1); node.parentNode = this.parentNode; list.splice(list.indexOf(this) + 1, 0, node); } insertBefore(node, ref) { node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1); node.parentNode = this; const at = ref ? this.childNodes.indexOf(ref) : -1; if (at < 0) this.childNodes.push(node); else this.childNodes.splice(at, 0, node); return node; } replaceChild(node, old) { const at = this.childNodes.indexOf(old); node.parentNode?.childNodes.splice(node.parentNode.childNodes.indexOf(node), 1); node.parentNode = this; this.childNodes[at] = node; old.parentNode = null; } replaceChildren(...kids) { this.childNodes.forEach((n) => (n.parentNode = null)); this.childNodes = []; this.append(...kids); } scrollIntoView() {} all() { return this.children.flatMap((c) => [c, ...c.all()]); } matches(selector) { return selector.split(",").some((one) => { const parts = one.trim().split(/\s*>\s*/); if (parts.length === 2) return ( matchOne(this, parts[1]) && !!this.parentElement && matchOne(this.parentElement, parts[0]) ); return matchOne(this, parts[0]); }); } closest(selector) { for (let at = this; at instanceof El; at = at.parentElement) if (at.matches(selector)) return at; return null; } querySelectorAll(selector) { return this.all().filter((n) => n.matches(selector)); } querySelector(selector) { return this.querySelectorAll(selector)[0] ?? null; } } function matchOne(el, compound) { const tag = compound.match(/^[a-z]+/i)?.[0]; if (tag && el.tagName !== tag.toUpperCase()) return false; for (const [, c] of compound.matchAll(/\.([\w-]+)/g)) if (!el.classList.contains(c)) return false; for (const [, k, v] of compound.matchAll(/\[([\w-]+)(?:="([^"]*)")?\]/g)) { if (k === "hidden") { if (!el.hidden) return false; } else if (v === undefined ? !(k in el.attrs) : el.attrs[k] !== v) return false; } return true; } const body = new El("body"); globalThis.Node = { TEXT_NODE: TEXT }; globalThis.document = { createElement: (tag) => new El(tag), createTextNode: (data) => new Text(data), createComment: () => new Comment(), querySelectorAll: (s) => body.querySelectorAll(s), querySelector: (s) => body.querySelector(s), }; function loadTs(file) { const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, require); return box.exports; } const dock = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Adjust_Dock.ts")); // ── 폼 흉내 — 명세 상자 = fieldset[data-spec-container] > legend(글 + [삭제]) ────────── const el = (tag, attrs = {}, ...kids) => { const node = new El(tag); for (const [k, v] of Object.entries(attrs)) { if (k === "class") node.className = v; else if (k === "hidden") node.hidden = v; else node.setAttribute(k, v); } node.append(...kids); return node; }; const removeButton = () => el("button", { "data-spec-remove": "chute" }, "삭제"); const box = (id, title, withRemove, ...kids) => { const legend = el("legend", {}, title); if (withRemove) legend.append(" ", removeButton()); return el("fieldset", { "data-spec-container": id }, legend, ...kids); }; const slots = { inlet: el("div"), outlet: el("div"), extra: el("div"), revetInlet: el("div"), revetOutlet: el("div"), wingInlet: el("div"), wingOutlet: el("div"), }; // 배관 명세(숨음 · 물넘이포장 고른 때) — 「관」 · 유입 · 유출 칸 const pipeSpec = el( "div", { class: "b05-structure__spec", hidden: true }, box("pipe", "관", false), box("inlet", "유입구", false, slots.inlet), box("outlet", "유출구", false, slots.outlet), box("revet_in", "기슭막이 유입", false, slots.revetInlet), box("revet_out", "기슭막이 유출", false, slots.revetOutlet), box("extra", "추가", false, slots.extra), ); // 보이는 명세 — 날개벽 유출 칸(괄호 붙임 + [삭제] 단추 상자) · 물넘이포장 경사각도 · 도수로/돌붙임 const wingOut = box("wing_out", "유출 날개벽", true, slots.wingOutlet); const ford = el( "div", { class: "b05-structure__spec" }, box("wing_in", "유입 날개벽", false, slots.wingInlet), wingOut, box("slope", "경사각도", false), box("chute", "도수로/돌붙임", true), ); body.append(pipeSpec, ford); dock.setAdjustSideSlots(slots); const legendOf = (sel) => { const fs_ = body.querySelector(sel); const legend = fs_.querySelector("legend"); return { active: fs_.classList.contains("is-active"), text: legend.childNodes.find((n) => n.nodeType === TEXT)?.textContent, buttons: legend.children.filter((c) => c.tagName === "BUTTON").map((c) => c.textContent), base: legend.dataset.baseLabel ?? null, }; }; const CHUTE = 'fieldset[data-spec-container="chute"]'; const SLOPE = 'fieldset[data-spec-container="slope"]'; const WING = 'fieldset[data-spec-container="wing_out"]'; const PIPE = 'fieldset[data-spec-container="pipe"]'; const panel = el("div"); const out = {}; // 294-1 ① 도수로 띠 고름 → 상자 강조 · 단추 그대로 panel.dataset.side = "chute"; dock.adoptAdjustPanel(panel); out.chuteOn = legendOf(CHUTE); // ② 날개벽 유출 고름 — 괄호 붙임(글 칸만) · 단추 그대로 · 도수로 강조 풂 panel.dataset.side = "outlet"; panel.dataset.panelTitle = "기슭막이"; dock.refreshAdjustSlots(); out.wingOn = legendOf(WING); out.chuteOff = legendOf(CHUTE); // 294-2 ③ 물넘이포장 콘크리트(side "pipe" · 관 상자 숨음) = 경사각도 상자 panel.dataset.side = "pipe"; dock.refreshAdjustSlots(); out.slopeOn = legendOf(SLOPE); out.pipeOn = legendOf(PIPE).active; out.wingOff = legendOf(WING); // ④ 풂 — 모두 원래 글 · 단추 panel.dataset.side = ""; dock.refreshAdjustSlots(); out.released = [CHUTE, WING, SLOPE].map((sel) => legendOf(sel)); // ⑤ 배관을 고른 때(배관 명세 보임 · 물넘이 숨음)는 그대로 「관」 상자 pipeSpec.hidden = false; ford.hidden = true; panel.dataset.side = "pipe"; dock.refreshAdjustSlots(); out.pipeVisible = { pipe: legendOf(PIPE).active, slope: legendOf(SLOPE).active }; console.log(JSON.stringify(out));