/* 창 아래 끝까지 칸(`ui_template/ui_template_data_table.ts` · PLAN 199-1) 단위검증 헬퍼 — * 트랜스파일해 가짜 DOM(요소 트리 · 창 · ResizeObserver)에서 돌리고 판정할 값을 JSON 으로 냄. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts"); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; // ── 가짜 DOM ── const listeners = { resize: new Set() }; const observers = []; const win = { innerHeight: 900, scrollY: 0, addEventListener: (t, f) => listeners[t]?.add(f), removeEventListener: (t, f) => listeners[t]?.delete(f), }; class RO { constructor(fn) { this.fn = fn; this.targets = new Set(); observers.push(this); } observe(el) { this.targets.add(el); } disconnect() { this.targets.clear(); } } function el(name, css = {}, height = 0) { const node = { name, css, height, top: 0, parentElement: null, nextElementSibling: null, isConnected: false, style: {}, getBoundingClientRect() { return { top: this.top - win.scrollY, height: this.height }; }, }; return node; } function link(parent, ...kids) { kids.forEach((k, i) => { k.parentElement = parent; k.nextElementSibling = kids[i + 1] ?? null; }); parent.kids = kids; return parent; } const sty = (n) => ({ rowGap: n.css.rowGap ?? "normal", paddingBottom: n.css.paddingBottom ?? "0px", borderBottomWidth: n.css.borderBottomWidth ?? "0px", marginBottom: n.css.marginBottom ?? "0px", }); const doc = { body: null }; const box = { exports: {} }; const sandbox = { window: win, document: doc, getComputedStyle: sty, ResizeObserver: RO, }; new Function("exports", "module", "require", ...Object.keys(sandbox), js)( box.exports, box, (p) => (p.endsWith(".css") ? {} : require(p)), ...Object.values(sandbox), ); const m = box.exports; // 문서: html > body(padding-bottom 8) > page(padding-bottom 16 · gap 8) > [tabs 40] [card(padding-bottom 12) > [root][note 40]] const html = el("html"); const body = el("body", { paddingBottom: "8px" }); const page = el("page", { paddingBottom: "16px", rowGap: "8px" }); const tabs = el("tabs", {}, 40); const card = el("card", { paddingBottom: "12px", marginBottom: "4px" }); const root = el("root"); const note = el("note", {}, 40); link(html, body); link(body, page); link(page, tabs, card); link(card, root, note); doc.body = body; root.top = 200; const out = { clamp: m.fillHeight(900, 200, 68), small: m.fillHeight(300, 250, 100) }; // 아래 자리 = note(40 + 줄 사이 0) + card 여백(12) + card 바깥 여백(4) + page 아래(16) + body 아래(8) out.below = m.spaceBelow(root); out.belowExpected = 40 + 0 + 12 + 4 + 16 + 8; // 아직 문서에 안 붙음 — 높이 안 건드림 let fits = 0; const stop = m.fillViewport(root, () => (fits += 1)); out.beforeAttach = root.style.maxHeight ?? null; out.observed = observers.some((o) => o.targets.has(root)) && observers.some((o) => o.targets.has(body)); // 붙음 → 관찰자가 부름 [html, body, page, tabs, card, root, note].forEach((n) => (n.isConnected = true)); observers.forEach((o) => o.fn()); out.first = root.style.maxHeight; out.fits = fits; // 창 크기 바뀜 → 다시 셈 win.innerHeight = 600; [...listeners.resize].forEach((f) => f()); out.afterResize = root.style.maxHeight; // 스크롤해도 같은 값(문서 좌표 기준) win.scrollY = 80; [...listeners.resize].forEach((f) => f()); out.afterScroll = root.style.maxHeight; win.scrollY = 0; // 아래 형제가 늘어남 → 그만큼 줄어듦 note.height = 100; observers.forEach((o) => o.fn()); out.afterNoteGrow = root.style.maxHeight; // 칸이 떨어지면 멈춤 root.isConnected = false; [...listeners.resize].forEach((f) => f()); out.listenersAfterDetach = listeners.resize.size; stop(); console.log(JSON.stringify(out));