"""235-1 B09 설계내역서 = 접히지 않는 트리 · 더블클릭 입력 · 고른 줄 상자.
- 폴더 줄(자식 있는 줄 · 제목 · 묶음) = 이름만 고침 · 수량 · 단가 빈칸 · 금액 = 자손 합 수식(읽기 전용 · 연결 없음)
- 파일 줄(항목) = 이름 · 규격 · 단위 · 수량 입력 + 「연결 ▾」
- 트리 안내선(├ is-tee · └ is-end · │ is-bar) · 폴더 · 파일 아이콘(공용 나무 꼴) = 공용 표 level.tree
- 공용 표 editOnDouble = 고칠 칸도 글 · 한 번 누름 = 줄 고름 · 더블클릭 · edit() = 그 칸만 글 상자 · Enter · 초점 떠남 = 끝
- 레벨 키 = 고른 줄에서 Enter · F2 = 입력 시작(편집 중 · 도구 단추 · 고른 줄 없음 = 안 먹음)
- 고른 줄 = 줄 전체 상자 테두리(테마 토큰 · 배경 대신)
"""
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from test_182_b09_bill_level import BOOKS, INPUTS, USER # noqa: E402
from test_184_3_b09_bill_menu import _node, _ts # noqa: E402
from test_200_2_b09_bill_level_bar import BAR_JS # noqa: E402
from test_184_3_b09_bill_menu import ROWS as FAKE_ROWS # noqa: E402
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
TABLE = IMPORTS.sub("", (ROOT / "ui_template" / "ui_template_data_table.ts").read_text("utf-8"))
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"]
SUMS = ["sum", "labor", "material", "expense"]
FOLDER_SVG, LEAF_SVG = "M1.5 3.5h4", "M3.5 1.5h6"
def _rows():
return compute_estimation(INPUTS, USER, None, BOOKS)["bill"]["rows"]
# --- 설계내역서 칸: 폴더 = 수식 · 파일 = 입력 ------------------------------------------------
SPEC_JS = """
const ctx = { sheets: { bill: { rows: __ROWS__, totals: null } }, payload: {}, user: {}, setUser: () => {} };
const spec = buildBill(ctx);
const keys = ["name", "spec", "unit", "qty", "price_sum", "sum", "labor", "material", "expense", "connect"];
console.log(JSON.stringify({
double: spec.editOnDouble, tree: spec.level.tree,
rows: spec.rows.map((r, i) => ({ key: r.key, kind: r.kind, connect: r.connect,
edit: keys.filter((k) => spec.editable(i, k)),
tips: Object.fromEntries(keys.map((k) => [k, spec.tip(i, k) ?? null])) })),
}));
"""
def _spec(rows):
return json.loads(
_node(_ts(SRC) + SPEC_JS.replace("__ROWS__", json.dumps(rows, ensure_ascii=False)))
)
def test_폴더_줄_이름만_수량_단가_빈칸_금액_자손_합():
rows = _rows()
by = {r["key"]: r for r in rows}
folders = [r for r in rows if r["kind"] != "item"]
assert {"sec:1", "sec:1.1", "hand:p", "hand:q"} <= {r["key"] for r in folders}
for f in folders:
assert f.get("qty") is None and all(f.get(f"price_{p}") is None for p in [*SUMS[1:], "sum"])
kids = [r for r in rows if r["kind"] == "item" and _under(by, r, f["key"])]
for p in SUMS: # 금액 = 자손 항목 합(셈이 냄)
assert f[p] == sum(r[p] or 0 for r in kids), (f["key"], p)
out = _spec(rows)
assert out["double"] is True and out["tree"] is True
for r in out["rows"]:
if r["kind"] == "item":
continue
assert r["edit"] == ["name"], r["key"] # 이름만
assert r["connect"] == "" # 연결 없음
for p in SUMS:
assert r["tips"][p] == "수식 = 아래 항목 합(읽기 전용)"
assert r["tips"]["qty"] is None and r["tips"]["name"] is None
def _under(by, row, key):
at = by.get(row.get("parent") or "")
while at:
if at["key"] == key:
return True
at = by.get(at.get("parent") or "")
return False
def test_파일_줄_입력_연결():
out = _spec(_rows())
items = [r for r in out["rows"] if r["kind"] == "item" and r["key"] != "fee"]
assert items
for r in items:
assert r["edit"] == ["name", "spec", "unit", "qty"], r["key"]
assert all(r["tips"][p] is None for p in SUMS)
linked = [r for r in items if not r["key"].startswith("mat:")]
assert linked and all(r["connect"].endswith("▾") for r in linked)
# --- 트리 안내선 · 아이콘 ---------------------------------------------------------------------
def _guides_py(levels):
"""거울 — 아래에 같은 레벨 형제(더 얕은 줄 전까지)가 있으면 ├ · 없으면 └ · 윗대 형제가 있으면 │."""
def more(i, d):
for lv in levels[i + 1 :]:
if lv < d:
return False
if lv == d:
return True
return False
out = []
for i, lv in enumerate(levels):
g = [("is-bar", d - 2) for d in range(2, lv) if more(i, d)]
if lv >= 2:
g.append(("is-tee" if more(i, lv) else "is-end", lv - 2))
out.append(g)
return out
GUIDE_JS = """
const parse = (h) => [...h.matchAll(/ui-data-table__guide (is-\\w+)" style="--g:(\\d+)"/g)].map((m) => [m[1], Number(m[2])]);
const levels = __LEVELS__;
const unit = treeGuides(levels).map(parse);
const ctx = { sheets: { bill: { rows: __ROWS__, totals: null } }, payload: {}, user: {}, setUser: () => {} };
const spec = buildBill(ctx);
const html = renderDataTableHtml({ columns: spec.columns, rows: spec.rows, level: spec.level,
editable: spec.editable, editOnDouble: true });
const trs = html.split("