"""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("")[1].split("")[0].split("").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("")[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("")[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 '흙깍기' in rows[0].replace(' class="is-merged"', "") assert 'class="is-merged"' in rows[0] assert ( rows[1].count("b' in rows[1] ) # 합쳐진 칸 빠짐 · 열 번호 assert '2' 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 'a' in rows[0] assert rows[0].count("ui-data-table__lv") == 1 # 지정 열만 assert '' 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_Earthwork_Tables.ts", 'mergeSame: ["group", "spec"]'), ("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["name"]'), ("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["group"]'), ("B08_Quantity/B08_Quantity_Material_Tables.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_Builders.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