Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-10-10 17:37:04 +09:00
7 changed files with 304 additions and 19 deletions
@@ -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) => {
@@ -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,
)
@@ -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;",
@@ -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 '<td class="ui-data-table__lv">a</td>' in rows[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" data-num="">' in rows[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\) \* 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"),
]
@@ -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("<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 굵게
+31 -3
View File
@@ -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;
+23 -6
View File
@@ -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>${tr}</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<DataTableOptions["level"]>, 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;