"""228-1 표 레벨 = 들여쓰기에 따라 레벨 · 글꼴(크기 · 굵기) 자동 — 공용 표 level 옵션 · 설계내역서 · 보기 전용 표. - 공용 표(`ui_template_data_table.ts`) level 칸 class = is-lv1~5(6 이상 = 5) · 묶음 줄 = is-node (group 이 주면 그 값 · 안 주면 바로 아래 줄 레벨이 더 큰 줄 · 그린 구간 밖 줄도 봄) - CSS = 묶음 굵게 · 크기 Lv1 16 · Lv2 15 · Lv3 14(기본) · Lv4 13 · Lv5 12 · 항목 줄은 14 보다 크지 않게 · 들여쓰기 한 단 = 표 기본 1em(레벨 글자 크기와 무관) - 설계내역서 = 들여 · 내어쓰기(메뉴 · 단추 · 키 · 끌기 같은 함수) → 셈 다시 → 그 줄과 자손 레벨 · 글꼴 바로 바뀜 - 보기 전용 2단 표(blockLevel) · 총괄 · 원가계산서 = 같은 규칙(머리 줄 16 굵게 · 내용 14 보통) """ 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_185_1_b09_total_level import SUMS # 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", "UI_Tabs_Bill_Drag", "UI_Tabs_Bill_Level"] # fmt: skip IN, OUT = "들여쓰기 →", "내어쓰기 ←" #: 줄마다 [레벨 칸 class(레벨 · 묶음만), --dt-lv] — 레벨 열 칸만 CLS = """ const cls = (html) => html.split("
")[1].split("")[0].split("").filter(Boolean) .map((r) => [(r.match(/class="([^"]*ui-data-table__lv[^"]*)"/) || [])[1]?.split(" ") .filter((c) => /^(ui-data-table__lv|is-lv\\d|is-node)$/.test(c)).join(" ") ?? null, Number((r.match(/--dt-lv:(\\d+)/) || [])[1])]); """ def _cls(body: str, names=SRC) -> dict: return json.loads(_node(_ts(names) + TABLE + CLS + body)) # --- 공용 표 ------------------------------------------------------------------------------ TABLE_JS = """ const columns = [{ key: "n", label: "명칭" }, { key: "v", label: "값", kind: "number" }]; const lv = [1, 2, 3, 4, 5, 6, 2, null]; const rows = lv.map((_, i) => ({ n: `r${i}`, v: i })); const of = (i) => lv[i]; console.log(JSON.stringify({ auto: cls(renderDataTableHtml({ columns, rows, level: { key: "n", of } })), given: cls(renderDataTableHtml({ columns, rows, level: { key: "n", of, group: (i) => i === 7 } })), win: cls(renderDataTableHtml({ columns, rows, level: { key: "n", of } }, { from: 2, to: 4, rowH: 28 })), edit: cls(renderDataTableHtml({ columns, rows, level: { key: "n", of }, editable: (i, k) => k === "n" })), })); """ def test_공용_표_레벨_글꼴_class(): out = _cls(TABLE_JS, []) assert out["auto"] == [ ["ui-data-table__lv is-lv1 is-node", 0], ["ui-data-table__lv is-lv2 is-node", 1], ["ui-data-table__lv is-lv3 is-node", 2], ["ui-data-table__lv is-lv4 is-node", 3], ["ui-data-table__lv is-lv5 is-node", 4], ["ui-data-table__lv is-lv5", 5], # 6 이상 = 5 글꼴 · 들여쓰기는 그대로 ["ui-data-table__lv is-lv2", 1], # 아래 줄이 얕음 = 항목 ["ui-data-table__lv is-lv1", 0], # 끝 줄 · 빈 레벨 = 1 ] assert [c for c, _ in out["given"]][6:] == [ "ui-data-table__lv is-lv2", "ui-data-table__lv is-lv1 is-node", ] assert "is-node" not in "".join(c for c, _ in out["given"][:6]) # 그린 구간(2~3줄)의 끝 줄도 구간 밖 아래 줄로 묶음을 셈 assert [c for c, _ in out["win"] if c] == [ "ui-data-table__lv is-lv3 is-node", "ui-data-table__lv is-lv4 is-node", ] assert out["edit"] == out["auto"] # 글 상자 칸도(글 상자 = font: inherit) def test_CSS_레벨_글꼴_규칙(): css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8") tokens = dict(re.findall(r"(--dt-(?:lv\d-size|indent)): ([^;]+);", css)) assert tokens == { "--dt-indent": "22px", "--dt-lv1-size": "var(--text-body)", "--dt-lv2-size": "15px", "--dt-lv4-size": "13px", "--dt-lv5-size": "var(--text-caption)", } theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") assert "--text-body: 16px;" in theme and "--text-body-sm: 14px;" in theme assert "--text-caption: 12px;" in theme def rule(sel: str) -> str: m = re.search( re.escape(f".ui-data-table__table td.ui-data-table__lv{sel} {{") + r"([^}]*)\}", css ) return m.group(1).strip() if m else "" assert rule(".is-node") == "font-weight: var(--font-weight-bold);" assert rule(".is-lv1.is-node") == "font-size: var(--dt-lv1-size);" assert rule(".is-lv2.is-node") == "font-size: var(--dt-lv2-size);" assert rule(".is-lv4") == "font-size: var(--dt-lv4-size);" assert rule(".is-lv5") == "font-size: var(--dt-lv5-size);" # 항목 Lv1 · 2 · Lv3 = 표 기본 14px(크기 규칙 없음) · 굵게 = 묶음만 assert not rule(".is-lv1") and not rule(".is-lv2") and not rule(".is-lv3") # 들여쓰기 = 글 칸 · 글 상자 · 나무 글 상자(235-1 · 아이콘 자리 더함) assert css.count("var(--dt-lv, 0) * var(--dt-indent)") == 3 and "* 1em" not in css # 줄 높이 28px 안 — 가장 큰 글자(16px × 줄 간격 1.3) assert "--dt-row-h: 28px;" in css and 16 * 1.3 < 28 # --- 설계내역서: 들여 · 내어쓰기 → 레벨 · 글꼴 바로 ------------------------------------------ BILL_JS = """ let user = __USER__; const sheets = { bill: { rows: __ROWS__, totals: null } }; const ctx = { sheets, payload: {}, get user() { return user; }, setUser: (u) => (user = u) }; const spec = buildBill(ctx); const html = cls(renderDataTableHtml({ columns: spec.columns, rows: spec.rows, level: spec.level, editable: spec.editable })); const out = { view: Object.fromEntries(spec.rows.map((r, i) => [r.key, html[i]])) }; const runs = __RUNS__; if (runs.length) { const [key, label] = runs[0]; const item = billMenu(ctx, spec.rows, spec.rows.findIndex((r) => r.key === key)) .find((x) => Array.isArray(x) && x[0] === label); item[1](); out.bill = user.bill; } console.log(JSON.stringify(out)); """ def _bill(user, run=None): rows = compute_estimation(INPUTS, user, None, BOOKS)["bill"]["rows"] body = ( BILL_JS.replace("__USER__", json.dumps(user, ensure_ascii=False)) .replace("__ROWS__", json.dumps(rows, ensure_ascii=False)) .replace("__RUNS__", json.dumps([run] if run else [], ensure_ascii=False)) ) return _cls(body) def _after(run): """메뉴 한 번(들여 · 내어쓰기) → 저장 꼴 → 셈 다시(화면 setUser 와 같은 길) → 다시 그림.""" bill = _bill(USER, run)["bill"] return _bill({**USER, "bill": bill})["view"] def test_내어쓰기_그_줄과_자손_레벨_글꼴_바로(): before = _bill(USER)["view"] # 손 묶음 g2(Lv4 · 자식 b · c Lv5) assert before["hand:g2"] == ["ui-data-table__lv is-lv4 is-node", 3] assert before["hand:b"] == ["ui-data-table__lv is-lv5", 4] assert before["hand:c"] == ["ui-data-table__lv is-lv5", 4] assert before["hand:g1"] == ["ui-data-table__lv is-lv3 is-node", 2] after = _after(["hand:g2", OUT]) assert after["hand:g2"] == ["ui-data-table__lv is-lv3 is-node", 2] # 14 굵게 assert after["hand:b"] == ["ui-data-table__lv is-lv4", 3] # 자손도 한 단 위 · 13 assert after["hand:c"] == ["ui-data-table__lv is-lv4", 3] assert after["hand:g1"] == before["hand:g1"] # 손 묶음 = 자식 없어도 묶음(group) def test_들여쓰기_항목이_묶음이_되면_굵게(): before = _bill(USER)["view"] assert before["earth:5"] == ["ui-data-table__lv is-lv3", 2] assert before["hand:x"] == ["ui-data-table__lv is-lv3", 2] after = _after(["earth:5", IN]) # 위 형제 hand:x 아래로 assert after["earth:5"] == ["ui-data-table__lv is-lv4", 3] assert after["hand:x"] == ["ui-data-table__lv is-lv3 is-node", 2] # 자식 생긴 항목 = 묶음 굵게 # 제목 줄 = 묶음 · Lv1 16 · Lv2 15 assert before["sec:1"] == ["ui-data-table__lv is-lv1 is-node", 0] assert before["sec:1.2"] == ["ui-data-table__lv is-lv2 is-node", 1] # --- 보기 전용 표: 같은 규칙 -------------------------------------------------------------- BLOCK_JS = """ const columns = [{ key: "name", label: "명칭" }]; const rows = ["머리", "줄1", "줄2", "합계", "머리2", "줄3"].map((name) => ({ name })); const top = (i) => [0, 3, 4].includes(i); console.log(JSON.stringify(cls(renderDataTableHtml({ columns, rows, level: blockLevel("name", top) })))); """ TOTAL_JS = f""" const user = {{ estimate: {{}} }}; const ctx = {{ sheets: {{ total: computeTotal({json.dumps(SUMS)}, user.estimate) }}, payload: {{}}, user, setUser: () => {{}} }}; const view = (t) => {{ const html = cls(renderDataTableHtml({{ columns: t.columns, rows: t.rows, level: t.level }})); return t.rows.map((r, i) => [r.key, r.level, html[i][0]]); }}; console.log(JSON.stringify({{ total: view(buildTotal(ctx)), cost: view(buildCost(ctx)) }})); """ def test_보기_전용_2단_표_머리_16_굵게_내용_14_보통(): out = _cls(BLOCK_JS) assert [c for c, _ in out] == [ "ui-data-table__lv is-lv1 is-node", "ui-data-table__lv is-lv2", "ui-data-table__lv is-lv2", "ui-data-table__lv is-lv1", # 합계(아래 줄 = 다음 머리) = 14 · 굵게는 줄 class is-title "ui-data-table__lv is-lv1 is-node", "ui-data-table__lv is-lv2", ] def test_총괄_원가계산서_같은_규칙(): out = _cls(TOTAL_JS, ["Calc_Base", "Calc_Total", "UI_Tabs", "UI_Tabs_Total"]) for tab in ("total", "cost"): rows = out[tab] for i, (key, lv, c) in enumerate(rows): node = i + 1 < len(rows) and (rows[i + 1][1] or 1) > (lv or 1) assert c == f"ui-data-table__lv is-lv{min(5, lv or 1)}" + ( " is-node" if node else "" ), (tab, key) cost = {k: c for k, _, c in out["cost"]} assert ( "is-lv3" in cost["direct_material"] and "is-node" not in cost["direct_material"] ) # 세목 14 보통 assert cost["material"].endswith("is-lv2 is-node") # 비목 소계(아래 세목) 15 굵게