diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts
index 314939f03..8363c7930 100644
--- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts
+++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts
@@ -96,7 +96,12 @@ export const buildBill: TabBuild = (ctx): BillTable => {
const title = r.kind === "title" ? "is-title" : r.kind === "group" ? "is-group" : "";
return [title, r.auto ? "" : "is-hand"].filter((c) => c).join(" ") || null;
},
- level: { key: "name", of: (i) => rows[i]?.level as number | undefined },
+ // 228-1 레벨 = 셈 나무 깊이(들여 · 내어쓰기 · 끌기 = 다시 셈 · 다시 그림) · 글꼴 = 레벨 · 묶음(빈 공종도)
+ level: {
+ key: "name",
+ of: (i) => rows[i]?.level as number | undefined,
+ group: (i) => rows[i]?.kind !== "item",
+ },
menu: (i) => billMenu(ctx, raw, i),
drop: (i, parent, before) => dropMove(ctx, raw, i, parent, before),
onCell: (i, key) => {
diff --git a/resources/tester/test_185_1_b09_total_level.py b/resources/tester/test_185_1_b09_total_level.py
index 6608e4ae1..896400301 100644
--- a/resources/tester/test_185_1_b09_total_level.py
+++ b/resources/tester/test_185_1_b09_total_level.py
@@ -71,6 +71,6 @@ def test_CSS_옛_레벨_규칙_없음_공용_표로():
assert "is-indent" not in css and "is-flat" not in css and "is-lv" not in css
common = (ROOT / "ui_template" / "ui_template_data_table.css").read_text(encoding="utf-8")
assert re.search(
- r"td\.ui-data-table__lv \{\s*padding-left: calc\(var\(--spacing-8\) \+ var\(--dt-lv, 0\) \* 1em\);",
+ r"td\.ui-data-table__lv \{\s*padding-left: calc\(var\(--spacing-8\) \+ var\(--dt-lv, 0\) \* var\(--dt-indent\)\);",
common,
)
diff --git a/resources/tester/test_200_2_b09_bill_level_bar.py b/resources/tester/test_200_2_b09_bill_level_bar.py
index 3a544d33c..f9d610b6c 100644
--- a/resources/tester/test_200_2_b09_bill_level_bar.py
+++ b/resources/tester/test_200_2_b09_bill_level_bar.py
@@ -1,5 +1,6 @@
"""200-2 B09 설계내역서 레벨 글 · 레벨 도구 줄 — 글 칸 왼쪽 맞춤 · 레벨 = 공종 · 명칭 칸 padding 들여쓰기 ·
-글자 크기 한 벌 · 단추 넷(고른 줄 · 못 하면 disabled) · 키 셋(Tab · Shift+Tab · Alt+↑↓) · 편집 중엔 키 안 먹음.
+글꼴 = 공용 표 레벨 글꼴(228-1 · 레벨 · 묶음 따라) · 단추 넷(고른 줄 · 못 하면 disabled) ·
+키 셋(Tab · Shift+Tab · Alt+↑↓) · 편집 중엔 키 안 먹음.
- 단추 · 키 = 우클릭 메뉴(billMenu) 같은 항목을 부름 — 줄 꼴 · 저장 꼴은 184-3 시험 그대로
- 표 · 문서 = 가짜 DOM(줄 tr · 글 칸 input) · 다시 그림 = buildBill + levelBar 다시
@@ -173,13 +174,16 @@ def test_키_셋_편집_중엔_안_먹음():
assert out["keyAct"] == [IN, OUT, UP, DOWN, None, UP, None]
-def test_왼쪽_맞춤_레벨_들여쓰기_글자_크기_한_벌():
+def test_왼쪽_맞춤_레벨_들여쓰기_글꼴_공용_표():
css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8")
bill = '.b09-estimation [data-b09-table="bill"]'
- # 201-1 레벨 들여쓰기 = 공용 표 level 옵션(test_201_1) · 레벨별 글자 크기 규칙 없음(한 벌)
+ # 레벨 들여쓰기 · 글꼴 = 공용 표 level 옵션(test_201_1 · test_228_1) · 페이지 CSS 에 따로 두지 않음
assert "is-lv" not in css and "font-size: 13px" not in css
+ common = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
+ assert "td.ui-data-table__lv.is-lv1.is-node" in common
src = (B09 / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8")
- assert 'level: { key: "name", of: (i) => rows[i]?.level' in src
+ assert "of: (i) => rows[i]?.level as number | undefined," in src
+ assert 'group: (i) => rows[i]?.kind !== "item",' in src
# 글 칸 왼쪽 맞춤(수량만 오른쪽)
assert re.search(
re.escape(f'{bill} .ui-data-table__input:not([data-k="qty"]) {{') + r"\s*text-align: left;",
diff --git a/resources/tester/test_201_1_table_level_merge.py b/resources/tester/test_201_1_table_level_merge.py
index dbcf7093d..de42f1eea 100644
--- a/resources/tester/test_201_1_table_level_merge.py
+++ b/resources/tester/test_201_1_table_level_merge.py
@@ -1,6 +1,7 @@
"""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 없음)
@@ -93,12 +94,13 @@ def test_mergeSame_같은_값만_rowspan():
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 'a | ' in rows[0]
assert rows[0].count("ui-data-table__lv") == 1 # 지정 열만
- assert '' in rows[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\) \* 1em\);"
+ 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)
@@ -117,7 +119,7 @@ APPLIED = [
("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", 'level: { key: "name"'),
+ ("B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts", 'group: (i) => rows[i]?.kind !== "item"'),
("B09_Estimation/B09_Estimation_UI_Shell.ts", "mergeSame: spec.mergeSame"),
]
diff --git a/resources/tester/test_228_1_table_level_font.py b/resources/tester/test_228_1_table_level_font.py
new file mode 100644
index 000000000..cd230f001
--- /dev/null
+++ b/resources/tester/test_228_1_table_level_font.py
@@ -0,0 +1,229 @@
+"""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": "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 굵게
diff --git a/ui_template/ui_template_data_table.css b/ui_template/ui_template_data_table.css
index 787d9e90f..76f9682eb 100644
--- a/ui_template/ui_template_data_table.css
+++ b/ui_template/ui_template_data_table.css
@@ -10,6 +10,13 @@
--dt-head: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface));
--dt-line: var(--color-border);
--dt-edge: color-mix(in srgb, var(--color-text-secondary) 45%, var(--color-border));
+ /* 들여쓰기 한 단 = 표 기본 글자 1em(레벨 글자 크기와 무관 · 끌기 한 단과 같음) */
+ --dt-indent: var(--text-body-sm);
+ /* 레벨 글자 크기(PLAN 228-1) — 줄 높이 28px 안 · Lv3 = 표 기본(14px) */
+ --dt-lv1-size: var(--text-body);
+ --dt-lv2-size: 15px;
+ --dt-lv4-size: 13px;
+ --dt-lv5-size: var(--text-caption);
width: fit-content; /* 표가 좁으면 틀도 표만큼 */
max-width: 100%;
max-height: calc(100vh - 260px);
@@ -173,9 +180,9 @@
outline-offset: -2px;
}
-/* 레벨(PLAN 201-1 · level 옵션 · 보기 전용) — 레벨 열 왼쪽 맞춤 · 레벨마다 1em 들여쓰기(글 상자도) */
+/* 레벨(PLAN 201-1 · level 옵션 · 보기 전용) — 레벨 열 왼쪽 맞춤 · 레벨마다 1em(표 기본) 들여쓰기(글 상자도) */
.ui-data-table__table td.ui-data-table__lv {
- padding-left: calc(var(--spacing-8) + var(--dt-lv, 0) * 1em);
+ padding-left: calc(var(--spacing-8) + var(--dt-lv, 0) * var(--dt-indent));
text-align: left;
}
@@ -184,10 +191,31 @@
}
.ui-data-table__table td.ui-data-table__lv .ui-data-table__input {
- padding-left: calc(var(--spacing-8) + var(--dt-lv, 0) * 1em);
+ padding-left: calc(var(--spacing-8) + var(--dt-lv, 0) * var(--dt-indent));
text-align: left;
}
+/* 레벨 글꼴(PLAN 228-1) — 묶음 줄(is-node) 굵게 · 크기는 레벨 · 항목 줄은 14px 보다 크지 않게(Lv1 · 2 항목 = 기본) */
+.ui-data-table__table td.ui-data-table__lv.is-node {
+ font-weight: var(--font-weight-bold);
+}
+
+.ui-data-table__table td.ui-data-table__lv.is-lv1.is-node {
+ font-size: var(--dt-lv1-size);
+}
+
+.ui-data-table__table td.ui-data-table__lv.is-lv2.is-node {
+ font-size: var(--dt-lv2-size);
+}
+
+.ui-data-table__table td.ui-data-table__lv.is-lv4 {
+ font-size: var(--dt-lv4-size);
+}
+
+.ui-data-table__table td.ui-data-table__lv.is-lv5 {
+ font-size: var(--dt-lv5-size);
+}
+
/* 같은 값 세로 병합(PLAN 201-1 · mergeSame) — 한 칸 글은 위에 */
.ui-data-table__table td.is-merged {
vertical-align: top;
diff --git a/ui_template/ui_template_data_table.ts b/ui_template/ui_template_data_table.ts
index 7f0b93d82..02c865ad1 100644
--- a/ui_template/ui_template_data_table.ts
+++ b/ui_template/ui_template_data_table.ts
@@ -12,6 +12,8 @@
* 줄 꼴(PLAN 155-2) = rowClass(줄) → 줄 class(예 is-title 굵게) · 안 주면 글 그대로
* 창 끝까지(PLAN 199) = fillViewport → 표 칸 최대 높이를 창 아래 끝까지(줄이 늘어도 페이지 스크롤 0)
* 레벨(PLAN 201-1 · 보기 전용) = level { key, of(줄) } → 그 열 왼쪽 맞춤 + 레벨마다 1em 들여쓰기(글 상자도)
+ * 글꼴(PLAN 228-1) = 칸 class is-lv1~5 · 묶음(group · 안 주면 바로 아래 줄이 더 깊은 줄) = is-node 굵게
+ * 크기 Lv1 16 · Lv2 15 · Lv3 14 · Lv4 13 · Lv5 12(px) · 항목 줄은 14 보다 크지 않게
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
* ========================================================================== */
@@ -68,8 +70,13 @@ export interface DataTableOptions {
cellClass?: (index: number, key: string) => string | null | undefined;
/** 참이면 표 칸 최대 높이 = 창 아래 끝까지(창 크기 바뀌면 다시 셈) — 안 주면 CSS 고정값 */
fillViewport?: boolean;
- /** 레벨(보기 전용) — key 열을 레벨마다 1em 들여씀 · of = 줄 레벨(1 = 맨 위 · 빈 값 = 1) */
- level?: { key: string; of: (index: number) => number | null | undefined };
+ /** 레벨(보기 전용) — key 열을 레벨마다 1em 들여씀 · of = 줄 레벨(1 = 맨 위 · 빈 값 = 1) ·
+ * group = 묶음 줄인지(안 주면 바로 아래 줄 레벨이 더 큰 줄) — 레벨 · 묶음 따라 글꼴(228-1) */
+ level?: {
+ key: string;
+ of: (index: number) => number | null | undefined;
+ group?: (index: number) => boolean;
+ };
/** 같은 값 세로 병합할 열 키(표마다 지정) — 위아래로 같은 글이 이어지면 rowSpan 한 칸 */
mergeSame?: string[];
}
@@ -160,7 +167,10 @@ function rowHtml(
const key = cols[j].key;
const span = spans?.[j] ?? 1;
if (!span) continue; // 위 칸에 합쳐짐
- const lvc = lv?.key === key ? " ui-data-table__lv" : "";
+ const lvc =
+ lv?.key === key
+ ? ` ui-data-table__lv is-lv${Math.min(5, lv.n + 1)}${lv.node ? " is-node" : ""}`
+ : "";
const c = spans ? ` data-c="${j}"` : "";
if (edit !== null && editable?.(edit, key)) {
const v = r[key];
@@ -199,9 +209,9 @@ function rowHtml(
: `${tr}
`;
}
-/** 줄 곁값 — lv = 레벨 열 · 들여쓸 칸 수 · spans = 열마다 rowSpan(0 = 위 칸에 합쳐짐) */
+/** 줄 곁값 — lv = 레벨 열 · 들여쓸 칸 수 · 묶음 줄인지 · spans = 열마다 rowSpan(0 = 위 칸에 합쳐짐) */
interface RowMore {
- lv?: { key: string; n: number };
+ lv?: { key: string; n: number; node: boolean };
spans?: number[];
}
@@ -276,6 +286,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
const spans = o.mergeSame?.length ? mergeSpans(o, from, to) : null;
for (let i = from; i < to; i++) {
const lvN = o.level ? Math.max(0, (o.level.of(i) ?? 1) - 1) : 0;
+ const node = o.level ? isNode(o.level, i, n) : false;
body += rowHtml(
cols,
fmts,
@@ -290,7 +301,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
o.rowClass?.(i),
o.cellClass ? (key) => o.cellClass?.(i, key) : undefined,
{
- ...(o.level ? { lv: { key: o.level.key, n: lvN } } : {}),
+ ...(o.level ? { lv: { key: o.level.key, n: lvN, node } } : {}),
...(spans ? { spans: spans[i - from] } : {}),
},
);
@@ -304,6 +315,12 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
);
}
+/** 묶음 줄(228-1) — group 이 주면 그 값 · 안 주면 바로 아래 줄 레벨이 더 큼 */
+function isNode(lv: NonNullable, i: number, n: number): boolean {
+ if (lv.group) return lv.group(i);
+ return i + 1 < n && (lv.of(i + 1) ?? 1) > (lv.of(i) ?? 1);
+}
+
/** 화면 위아래로 더 그려 둘 줄 수 · 구간은 이 단위로 끊어 스크롤마다 다시 그리지 않음 */
const OVER = 40;