- 새 공용 부품 ui_template_hopyo_grid(.ts · .css) — 호표 머리 줄(이름 · 규격 · 단위 · 1 단위 합계) · 구성 줄(품명 · 규격 · 수량 · 단위 · 합계 · 노무비 · 재료비 · 경비 단가 / 금액 · 비고 · 구분 꼬리표) · 합계 줄 빈 줄 붉게 · 부품 모음 견본 칸 - M02 Labor_Grid — 문서 + 서버 calc 답 그대로(단가 = 기준 ÷ 배 · 금액 = 반영 · 합계 = 반영합 · 수량 없음 줄 = 빈 줄) · Labor_Detail 맨 위 격자(카드 · 상세 같이) · 읽기만 카드 = 격자만 - B09 일위대가표(호표보기 · 박스보기 바탕 줄) = 같은 부품 · 줄 값 · 이동 · 줄 꼴 그대로 + 구분 칸 · 머리 1 단위 합계 - 시험: test_704_3 새로 · B09 표 시험 다섯(155 · 157-4 · 201-1 · 202-1 · 304)은 격자 파일을 이어 붙이게 고침(줄 글이 공용 부품으로 옮김) · hopyo_screen 상세 줄 수 693 → 700 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
6.4 KiB
Python
151 lines
6.4 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"
|
|
GRID = "ui_template/ui_template_hopyo_grid.ts" # 704-3 일위대가표 격자(Tabs_Labor 앞)
|
|
|
|
# 기억(b_page_state) 대신 Map · 탭 줄(fillTabs) 대신 가짜 button 줄 · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
|
|
STUBS = r"""
|
|
const kept = new Map();
|
|
const dateTimeText = (iso) => String(iso).replace("T", " "); // 593-1 PC 시간대 글 — 견본은 시간대 없음
|
|
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;
|
|
};
|
|
const fillTabs = (host, o) => {
|
|
const kids = o.tabs.map((t) => {
|
|
const b = document.createElement("button");
|
|
b.textContent = t.label;
|
|
b.dataset[o.data] = t.key;
|
|
b.addEventListener("click", () => t.key !== o.value && o.onChange(t.key));
|
|
return b;
|
|
});
|
|
host.replaceChildren(...kids);
|
|
return kids;
|
|
};
|
|
const createSegment = (o) => {
|
|
const root = document.createElement("div");
|
|
let current = o.value;
|
|
const items = o.options.map((x) => {
|
|
const b = document.createElement("button");
|
|
b.textContent = x.label;
|
|
b.dataset.value = x.value;
|
|
b.addEventListener("click", () => {
|
|
if (current === x.value) return;
|
|
current = x.value;
|
|
mark();
|
|
o.onChange(x.value);
|
|
});
|
|
return b;
|
|
});
|
|
const mark = () => items.forEach((b) => b.setAttribute("aria-checked", String(b.dataset.value === current)));
|
|
mark();
|
|
root.append(...items);
|
|
return { root, get: () => current, set: (v) => { current = v; mark(); } };
|
|
};
|
|
const createText = (text, kind) => {
|
|
const p = document.createElement("p");
|
|
p.className = `ui-text ui-text--${kind}`;
|
|
p.textContent = text;
|
|
return p;
|
|
};
|
|
const createBadge = (text, tone) => {
|
|
const span = document.createElement("span");
|
|
span.className = `ui-badge ui-badge--${tone}`;
|
|
span.textContent = text;
|
|
return span;
|
|
};
|
|
const createToolbar = () => document.createElement("div");
|
|
const hoverCalls = [];
|
|
const attachHoverInfo = (root, text, selector) => { hoverCalls.push({ root, text, selector }); return () => {}; };
|
|
"""
|
|
|
|
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); } }
|
|
replaceChildren(...xs) { this.children = []; this.append(...xs); }
|
|
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.includes(",")) return sel.split(",").some((s) => this.matches(s.trim()));
|
|
const tag = sel.match(/^([a-z]+)([.[].*)$/); // tr.is-pick · tr[data-i="3"]
|
|
if (tag) return this.matches(tag[1]) && this.matches(tag[2]);
|
|
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; }
|
|
querySelectorAll(sel) { return this.all(sel); }
|
|
get isConnected() { return true; }
|
|
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)); }
|
|
}
|
|
const docKeys = []; // 248-1 문서 keydown — key(이름, 대상) 로 부름
|
|
globalThis.document = { createElement: (t) => new E(t), addEventListener: (t, f) => t === "keydown" && docKeys.push(f) };
|
|
const key = (k, target = null) => docKeys.forEach((f) => f({ key: k, target }));
|
|
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)
|