From 833e4146a92c446999ebfc09a051647e9e58e21e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 17:36:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(table):=20=ED=91=9C=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=20=EA=B8=80=EA=BC=B4=20=EC=9E=90=EB=8F=99=20=E2=80=94=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=20=ED=81=AC=EA=B8=B0=20=C2=B7=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EA=B5=B5=EA=B2=8C=20=C2=B7=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EB=82=B4=EC=97=AD=EC=84=9C=20=C2=B7=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=ED=91=9C=20=EA=B0=99=EC=9D=80=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99(228-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공용 표 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 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts | 7 +- .../tester/test_185_1_b09_total_level.py | 2 +- .../tester/test_200_2_b09_bill_level_bar.py | 12 +- .../tester/test_201_1_table_level_merge.py | 10 +- .../tester/test_228_1_table_level_font.py | 229 ++++++++++++++++++ ui_template/ui_template_data_table.css | 34 ++- ui_template/ui_template_data_table.ts | 29 ++- 7 files changed, 304 insertions(+), 19 deletions(-) create mode 100644 resources/tester/test_228_1_table_level_font.py 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;