Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
136 lines
4.1 KiB
JavaScript
136 lines
4.1 KiB
JavaScript
/* 창 아래 끝까지 칸(`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));
|