Files
Aislo/resources/tester/test_201_1_table_level_merge.py
T
eomsangdonandClaude Opus 5.5 833e4146a9 feat(table): 표 레벨 글꼴 자동 — 레벨 크기 · 묶음 굵게 · 설계내역서 · 보기 전용 표 같은 규칙(228-1)
- 공용 표 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
2026-10-10 17:36:07 +09:00

138 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""201-1 공용 표 level(보기 전용) · mergeSame(같은 값 세로 병합) 옵션 + B08 · B09 적용 표.
- level { key, of } = 그 열 칸 class ui-data-table__lv · 줄 style --dt-lv(레벨 − 1) · 글 상자 칸도 · 다루기 단추 없음
· 레벨 글꼴 class is-lv1~5 · 묶음 is-node(228-1 · test_228_1)
- mergeSame [열] = 위아래 같은 글이 이어지면 rowspan · 앞 병합 열이 갈리면 갈림 · 빈 값 · 빈칸 글 · 고칠 칸 안 합침 ·
합계 줄 안 합침 · 그린 구간(win) 안에서만 · 병합 표 칸 = data-c(열 번호)
- 옵션 없으면 무변(rowspan · data-c · --dt-lv 없음)
"""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
JS = r"""
const fs = require("fs");
const path = require("path");
const ts = require(path.join(process.argv[1], "config", "node_modules", "typescript"));
const file = path.join(process.argv[1], "ui_template", "ui_template_data_table.ts");
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, (p) => (p.endsWith(".css") ? {} : require(p)));
const m = box.exports;
const body = (html) => html.split("<tbody>")[1].split("</tbody>")[0].split("</tr>").filter(Boolean);
const columns = [
{ key: "g", label: "구분" }, { key: "s", label: "규격" }, { key: "n", label: "명칭" },
{ key: "v", label: "값", kind: "number" },
];
const rows = [
{ g: "흙깍기", s: "가", n: "a", v: 1 },
{ g: "흙깍기", s: "가", n: "b", v: 1 },
{ g: "흙깍기", s: "나", n: "c", v: 1 },
{ g: "운반", s: "나", n: "d", v: 2 },
{ g: "운반", s: " ", n: "e", v: 2 },
{ g: "", s: " ", n: "f", v: 2 },
{ g: "", s: "다", n: "g", v: 3 },
];
const total = { g: "계", v: 12 };
const out = {};
out.plain = m.renderDataTableHtml({ columns, rows, total });
const merged = { columns, rows, total, mergeSame: ["g", "s"] };
out.merged = body(m.renderDataTableHtml(merged));
out.mergedFoot = m.renderDataTableHtml(merged).split("<tfoot>")[1];
out.spans = m.mergeSpans(merged, 0, rows.length);
out.win = m.mergeSpans(merged, 1, 4);
out.edit = m.mergeSpans({ ...merged, editable: (i, k) => i === 1 && k === "g" }, 0, 3);
out.level = body(m.renderDataTableHtml({
columns, rows: rows.slice(0, 3), level: { key: "n", of: (i) => [1, 2, null][i] },
editable: (i, k) => i === 1 && k === "n",
}));
out.cell = [m.cellColumn({ dataset: { c: "3" }, cellIndex: 1 }), m.cellColumn({ dataset: {}, cellIndex: 1 })];
console.log(JSON.stringify(out));
"""
def _run() -> dict:
result = subprocess.run(
["node", "-e", JS, str(ROOT)], capture_output=True, text=True, encoding="utf-8", timeout=60
)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_옵션_없으면_무변():
html = _run()["plain"]
assert "rowspan" not in html.split("<tbody>")[1]
assert "data-c=" not in html and "--dt-lv" not in html and "ui-data-table__lv" not in html
def test_mergeSame_같은_값만_rowspan():
out = _run()
# 구분: 흙깍기 3 · 운반 2 · 빈 값 2(안 합침) / 규격: 가 2(흙깍기 안) · 나 = 구분이 갈려 따로 · 빈칸 글 안 합침
assert [s[0] for s in out["spans"]] == [3, 0, 0, 2, 0, 1, 1]
assert [s[1] for s in out["spans"]] == [2, 0, 1, 1, 1, 1, 1]
assert all(s[2] == 1 and s[3] == 1 for s in out["spans"]) # 지정 안 한 열 = 그대로
rows = out["merged"]
assert '<td rowspan="3" data-c="0">흙깍기</td>' in rows[0].replace(' class="is-merged"', "")
assert 'class="is-merged"' in rows[0]
assert (
rows[1].count("<td") == 2 and '<td data-c="2">b</td>' in rows[1]
) # 합쳐진 칸 빠짐 · 열 번호
assert '<td data-num="" data-c="3">2</td>' in rows[4] # 숫자 값 그대로
assert "rowspan" not in out["mergedFoot"] # 합계 줄 안 합침
# 그린 구간(1~3줄) 안에서만 · 고칠 칸 안 합침
assert out["win"] == [[2, 1, 1, 1], [0, 1, 1, 1], [1, 1, 1, 1]]
assert [s[0] for s in out["edit"]] == [1, 1, 1]
assert out["cell"] == [3, 1]
def test_level_보기_전용_들여쓰기():
rows = _run()["level"]
assert [re.search(r'style="--dt-lv:(\d)"', r).group(1) for r in rows] == ["0", "1", "0"]
# 아래 줄이 더 깊음 = 묶음
assert '<td class="ui-data-table__lv is-lv1 is-node">a</td>' in rows[0]
assert rows[0].count("ui-data-table__lv") == 1 # 지정 열만
assert '<td class="is-edit ui-data-table__lv is-lv2" data-num="">' in rows[1] # 글 상자 칸도
assert "button" not in "".join(rows) # 다루기 단추 없음(설계내역서만)
css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
pad = r"padding-left: calc\(var\(--spacing-8\) \+ var\(--dt-lv, 0\) \* var\(--dt-indent\)\);"
assert re.search(r"td\.ui-data-table__lv \{\s*" + pad + r"\s*text-align: left;", css)
assert re.search(r"td\.ui-data-table__lv \.ui-data-table__input \{\s*" + pad, css)
assert re.search(r"td\.is-merged \{\s*vertical-align: top;", css)
#: 적용 표 — [파일, 들어 있어야 할 글]
APPLIED = [
("B08_Quantity/B08_Quantity_UI_Earthwork_Sheets.ts", 'mergeSame: ["group", "spec"]'),
("B08_Quantity/B08_Quantity_UI_Material.ts", 'mergeSame: ["name"]'),
("B08_Quantity/B08_Quantity_UI_Material.ts", 'mergeSame: ["group"]'),
("B08_Quantity/B08_Quantity_UI_Material.ts", "mergeSame: built.mergeSame"),
("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'mergeSame: ["part"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'level: blockLevel("name"'),
("B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts", 'level: blockLevel("name"'),
("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'level: blockLevel("text"'),
("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'mergeSame: ["kind"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Total.ts", 'mergeSame: ["group", "part"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Total.ts", 'mergeSame: ["part"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts", 'group: (i) => rows[i]?.kind !== "item"'),
("B09_Estimation/B09_Estimation_UI_Shell.ts", "mergeSame: spec.mergeSame"),
]
def test_B08_B09_적용():
for path, text in APPLIED:
assert text in (ROOT / path).read_text("utf-8"), (path, text)
# 옛 글 빈칸 지우기 · 앞 빈칸 들여쓰기는 없앰(병합 · level 로 갈음)
mat = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text("utf-8")
assert '=== row.name ? ""' not in mat and '=== row.group ? ""' not in mat
labor = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts").read_text("utf-8")
assert 'const CHILD = "ㄴ "' in labor and '" 합 계"' not in labor
# 셸 칸 → 열 = cellColumn(병합 표에서 cellIndex 가 밀림)
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert ".cellIndex" not in shell and shell.count("cellColumn(") == 2