- 171-2 새 공용 ui_template_group_cards(.ts · .css) — 머리(호표 · 명칭 · 규격 · 단위 · 합계) · 몸 줄(이름 · 규격 · 수량 · 단위 · 합계 · 비고) · 합계 줄(노무 · 재료 · 경비 · 계) - 글자 14px 한 벌 · 줄 간격 좁게(공용 카드 16px 끼움 없음) · 숫자 오른쪽 · 줄 금액 = 합계만 + 비목 셋 풍선 - 가로 열 수 1~4(기본 2) · 묶음 12 넘으면 요약 보기(머리 · 합계만 · 머리 누름 = 펼침 · 모두 펼침 · 접기) - 157-2 산근 카드 파일 갈음(지움) - 171-3 일위대가표 · 중기사용료 · 단가산출근거 = 카드 기본 · [표로 보기] = 표 그대로 · 보기 · 열 수 기억(b_page_state 두 줄 · 탭마다) - 값 · 풍선 · 칸 누름 이동 = 표 줄 그대로 · 집계표 · 설계내역서 · B08 대상 밖 - 시험 test_171_2 · test_171_3 · 공용 도우미(가짜 DOM) · test_157_2 갈음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
95 lines
4.0 KiB
Python
95 lines
4.0 KiB
Python
"""171 묶음 카드 시험 도우미 — TS 소스를 이어 붙여 Node(타입 지움)로 돌림 · 작은 가짜 DOM.
|
|
|
|
시험 파일 아님(test_ 머리 없음) — test_171_2 · test_171_3 · test_157_2 가 씀.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
|
|
|
COMMON = "ui_template/ui_template_group_cards.ts"
|
|
TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts"
|
|
GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts"
|
|
|
|
# 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 157-3 원문번호 풍선(raw) 짝
|
|
STUBS = r"""
|
|
const kept = new Map();
|
|
const readState = (name) => (kept.has(name) ? JSON.parse(kept.get(name)) : null);
|
|
const writeState = (name, value) => kept.set(name, JSON.stringify(value));
|
|
const createButton = (o) => {
|
|
const b = document.createElement("button");
|
|
b.className = `ui-btn ui-btn--${o.variant}`;
|
|
b.textContent = o.label;
|
|
if (o.onClick) b.addEventListener("click", o.onClick);
|
|
return b;
|
|
};
|
|
"""
|
|
|
|
RAW = r"""
|
|
const raw = (shown, codes) => (codes && codes !== shown ? `원문번호 ${String(codes)}` : null);
|
|
const SOURCES = [];
|
|
const openQuotes = () => {};
|
|
"""
|
|
|
|
FAKE_DOM = r"""
|
|
class E {
|
|
constructor(tag) {
|
|
this.tagName = tag.toUpperCase();
|
|
this.children = []; this.parent = null; this.dataset = {}; this.attrs = {};
|
|
this.cls = new Set(); this.textContent = ""; this.title = ""; this.on = {};
|
|
const props = {};
|
|
this.style = { props, setProperty: (k, v) => { props[k] = v; } };
|
|
const self = this;
|
|
this.classList = {
|
|
add: (...c) => c.forEach((x) => self.cls.add(x)),
|
|
remove: (...c) => c.forEach((x) => self.cls.delete(x)),
|
|
contains: (c) => self.cls.has(c),
|
|
toggle: (c, force) => {
|
|
const on = force === undefined ? !self.cls.has(c) : force;
|
|
on ? self.cls.add(c) : self.cls.delete(c);
|
|
return on;
|
|
},
|
|
};
|
|
}
|
|
get className() { return [...this.cls].join(" "); }
|
|
set className(v) { this.cls = new Set(String(v).split(/\s+/).filter(Boolean)); }
|
|
append(...xs) { for (const x of xs) { x.parent = this; this.children.push(x); } }
|
|
prepend(...xs) { for (const x of xs.reverse()) { x.parent = this; this.children.unshift(x); } }
|
|
addEventListener(t, f) { (this.on[t] ??= []).push(f); }
|
|
setAttribute(k, v) { this.attrs[k] = String(v); }
|
|
scrollIntoView() { this.scrolled = true; }
|
|
matches(sel) {
|
|
if (sel.startsWith(".")) return this.cls.has(sel.slice(1));
|
|
const m = sel.match(/^\[data-([\w-]+)(?:="(.*)")?\]$/);
|
|
const k = m ? m[1].replace(/-(\w)/g, (_, c) => c.toUpperCase()) : "";
|
|
if (m) return m[2] === undefined ? k in this.dataset : this.dataset[k] === m[2];
|
|
return this.tagName === sel.toUpperCase();
|
|
}
|
|
closest(sel) { for (let e = this; e; e = e.parent) if (e.matches(sel)) return e; return null; }
|
|
all(sel, out = []) { for (const c of this.children) { if (c.matches(sel)) out.push(c); c.all(sel, out); } return out; }
|
|
querySelector(sel) { return this.all(sel)[0] ?? null; }
|
|
contains(x) { for (let e = x; e; e = e.parent) if (e === this) return true; return false; }
|
|
get text() { return this.textContent + this.children.map((c) => c.text).join(""); }
|
|
click() { const event = { target: this }; for (let e = this; e; e = e.parent) (e.on.click ?? []).forEach((f) => f(event)); }
|
|
}
|
|
globalThis.document = { createElement: (t) => new E(t) };
|
|
globalThis.requestAnimationFrame = (f) => f();
|
|
"""
|
|
|
|
|
|
def run(files, js_body, prelude=""):
|
|
"""`files` 차례로 이어 붙인 소스(import 지움) + 가짜 DOM + `js_body` → stdout JSON."""
|
|
src = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files)
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript"],
|
|
input=FAKE_DOM + STUBS + prelude + src + js_body,
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|