- 공용 표 level 옵션: 레벨 칸 class is-lv1~5 · 묶음 줄 is-node(group 지정 · 안 주면 바로 아래 줄이 더 깊은 줄) - 크기 Lv1 16 · Lv2 15 · Lv3 14 · Lv4 13 · Lv5 12(토큰) · 굵게 = 묶음 · 항목 줄은 14 이하 - 들여쓰기 한 단 = 표 기본 1em 토큰(레벨 글자 크기와 무관 · 끌기 한 단과 같음) - 설계내역서 group = 항목 아닌 줄(빈 공종도 묶음) · 들여 · 내어쓰기는 셈 다시 → 레벨 · 글꼴 바로 - 새 시험 test_228_1 · test_200_2 · 201_1 · 185_1 기대값 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
230 lines
10 KiB
Python
230 lines
10 KiB
Python
"""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("<tbody>")[1].split("</tbody>")[0].split("</tr>").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": "var(--text-body-sm)",
|
|
"--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")
|
|
assert css.count("var(--dt-lv, 0) * var(--dt-indent)") == 2 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 굵게
|