feat(ui,b09): 공용 표 줄 꼴 곁값 · 일위대가표 제목 굵게 · 하위 「ㄴ」(155-2)
- 공용 표 rowClass(줄) 곁값 — 본문 줄 class · 안 주면 글 그대로 · is-title 굵게 - 공용 표 칸 앞 빈칸 · 겹 빈칸 접지 않음(white-space-collapse: preserve-spaces) - B09 일위대가표 = 머리 · 합계 굵게 · 품셈 인용 · 줄 = 들여쓰기 「ㄴ」(2단) - 시험 test_ui_data_table 줄 꼴 · 새 test_155_b09_tabs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -199,6 +199,7 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
caption: TABS.find(([id]) => id === tab)?.[1],
|
||||
editable: spec.editable,
|
||||
onEdit: spec.onEdit,
|
||||
rowClass: spec.rowClass,
|
||||
onCellClick: (i, key) => {
|
||||
if (spec.onCell) spec.onCell(i, key);
|
||||
if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface TabTable {
|
||||
onEdit?: (index: number, key: string, text: string) => void;
|
||||
/** 칸 누름 → 그 자리 동작(예 관급 · 별도 표시 바꿈) */
|
||||
onCell?: (index: number, key: string) => void;
|
||||
/** 줄 꼴 class(공용 표 `rowClass` · 예 제목 줄 굵게) */
|
||||
rowClass?: (index: number) => string | null | undefined;
|
||||
}
|
||||
|
||||
export type TabBuild = (ctx: TabContext) => TabTable | string;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*
|
||||
* 목록표 = 호표 한 줄(합계 · 노무비 · 재료비 · 경비) · 줄 누름 = 그 블록으로
|
||||
* 일위대가표 = 블록(머리 · [품셈 인용] · 줄 · 합계) · 줄 누름 = 단가를 낸 탭 · 줄(대가 줄 = 그 블록)
|
||||
* 줄 꼴(155-2) = 머리 · 합계 굵게(`is-title`) · 인용 · 줄 = 들여쓰기 「ㄴ」(2단 · 겹 대가는 안 펼침)
|
||||
* 연결된 구조물 조합 사본이 없으면 빈 표 「연결 없음」
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
|
||||
const EMPTY =
|
||||
"연결 없음 — 연결된 구조물 조합 사본(일위대가)이 없음 · B05 · B06 에서 조합을 고르고 [저장]";
|
||||
/** 하위 줄 앞 — 들여쓰기 + 「ㄴ」(공용 표가 앞 빈칸을 접지 않음) */
|
||||
const CHILD = " ㄴ ";
|
||||
const ROUNDS = "줄 금액 ROUNDDOWN(단가 × 수량, 1) · % 줄 = 밑수 × % / 100 · 합계 ROUNDDOWN 0";
|
||||
|
||||
/** 블록 줄 → 단가를 낸 탭 */
|
||||
@@ -59,7 +62,9 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
if (!blocks.length) return EMPTY;
|
||||
const rows: DataRow[] = [];
|
||||
const links: Array<Link | null> = [];
|
||||
const titles = new Set<number>();
|
||||
for (const b of blocks) {
|
||||
titles.add(rows.length);
|
||||
rows.push({
|
||||
key: b.id,
|
||||
name: `제 ${b.no} 호표 ${b.name}`,
|
||||
@@ -69,13 +74,13 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
});
|
||||
links.push(null);
|
||||
if (b.cite) {
|
||||
rows.push({ key: `${b.id}|cite`, name: ` ${b.cite}`, note: "품셈 인용" });
|
||||
rows.push({ key: `${b.id}|cite`, name: `${CHILD}${b.cite}`, note: "품셈 인용" });
|
||||
links.push(null);
|
||||
}
|
||||
b.lines.forEach((ln: Row, n: number) => {
|
||||
rows.push({
|
||||
key: `${b.id}|${n}`,
|
||||
name: ` ${ln.name || ""}`,
|
||||
name: `${CHILD}${ln.name || ""}`,
|
||||
spec: ln.spec || "",
|
||||
qty: ln.qty,
|
||||
unit: ln.unit || "",
|
||||
@@ -92,6 +97,7 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
const tab = LINE_TAB[ln.kind];
|
||||
links.push(tab && ln.key ? { tab, key: String(ln.key) } : null);
|
||||
});
|
||||
titles.add(rows.length);
|
||||
rows.push({
|
||||
key: `${b.id}|합계`,
|
||||
name: " 합 계",
|
||||
@@ -122,5 +128,6 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
frozen: 1,
|
||||
hint: `단가 = B09 프로젝트 한 벌 굳힌 단가 · ${ROUNDS} · 줄 누름 = 단가를 낸 탭(대가 줄 = 그 블록)`,
|
||||
link: (i: number) => links[i] ?? null,
|
||||
rowClass: (i: number) => (titles.has(i) ? "is-title" : null),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -90,6 +90,12 @@ console.log(
|
||||
}),
|
||||
escaped: m.renderDataTableHtml({ columns: [{ key: "x", label: "<b>" }], rows: [{ x: "a&<" }] }),
|
||||
noTotal: m.renderDataTableHtml({ columns: [tc], rows: [] }).includes("<tfoot>"),
|
||||
rowClass: m.renderDataTableHtml({
|
||||
columns: [{ key: "x", label: "X" }],
|
||||
rows: [{ x: "제 1 호표" }, { x: " ㄴ 석공" }],
|
||||
total: { x: 3 },
|
||||
rowClass: (i) => (i === 0 ? "is-title" : null),
|
||||
}),
|
||||
scale: m.SCALE,
|
||||
widths: m.renderDataTableHtml({
|
||||
columns: [
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""155 B09 탭 화면 꼴 — 일위대가표 줄 꼴(155-2).
|
||||
|
||||
`B09_Estimation_UI_Tabs.ts` + `B09_Estimation_UI_Tabs_Labor.ts` 를 Node(타입 지움)로 돌려
|
||||
견본 블록 하나(머리 · 품셈 인용 · 줄 둘 · 합계)의 표 줄 글 · 줄 꼴을 판정.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
|
||||
BLOCK = {
|
||||
"id": "DG1",
|
||||
"no": 1,
|
||||
"name": "찰쌓기",
|
||||
"spec": "",
|
||||
"unit": "m2",
|
||||
"kind": "품셈",
|
||||
"cite": "건설표준품셈 7-1-2",
|
||||
"lines": [
|
||||
{"kind": "labor", "key": "L00005", "name": "석공", "qty": 0.072, "unit": "인",
|
||||
"price": 258935, "amount": 18643.3, "ref_no": "노무3",
|
||||
"prices": {"labor": 258935, "material": None, "expense": None},
|
||||
"amounts": {"labor": 18643.3, "material": None, "expense": None}},
|
||||
{"kind": "sangeun", "key": "SG1", "name": "뒷채움", "qty": 0.5, "unit": "m3",
|
||||
"price": 8328, "amount": 4164, "ref_no": "산근 1호표",
|
||||
"prices": {"labor": 6878, "material": 538, "expense": 912},
|
||||
"amounts": {"labor": 3439, "material": 269, "expense": 456}},
|
||||
],
|
||||
"sum": 22807, "labor": 22082, "material": 269, "expense": 456,
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def _node(js_body):
|
||||
src = "".join(
|
||||
IMPORTS.sub("", (ROOT / "B09_Estimation" / f"B09_Estimation_{n}.ts").read_text("utf-8"))
|
||||
for n in ("UI_Tabs", "UI_Tabs_Labor")
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + js_body],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_155_2_일위대가표_제목_굵게_하위_ㄴ():
|
||||
out = _node(
|
||||
f"\nconst t = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(BLOCK)}] }} }});"
|
||||
"\nconsole.log(JSON.stringify({ names: t.rows.map((r) => r.name),"
|
||||
" cls: t.rows.map((_, i) => t.rowClass(i) ?? null) }));"
|
||||
)
|
||||
assert out["names"] == [
|
||||
"제 1 호표 찰쌓기",
|
||||
" ㄴ 건설표준품셈 7-1-2",
|
||||
" ㄴ 석공",
|
||||
" ㄴ 뒷채움",
|
||||
" 합 계",
|
||||
]
|
||||
assert out["cls"] == ["is-title", None, None, None, "is-title"]
|
||||
@@ -112,3 +112,13 @@ def test_155_1_글자_14px_열_폭_같은_비율(r):
|
||||
assert 'class="is-frozen is-edge" style="left:112px"' in w # 둘째 고정 열 left 도 같이
|
||||
assert "font-size: var(--text-body-sm);" in r["css"]
|
||||
assert "--dt-row-h: 28px;" in r["css"]
|
||||
|
||||
|
||||
def test_155_2_줄_꼴_곁값(r):
|
||||
# 155-2 — rowClass 가 낸 class 를 본문 줄에만 · 빈 값이면 안 붙임 · 앞 빈칸을 접지 않는 서식
|
||||
rc = r["rowClass"]
|
||||
assert '<tr class="is-title"><td>제 1 호표</td></tr>' in rc
|
||||
assert '<tr class="is-alt"><td> ㄴ 석공</td></tr>' in rc
|
||||
assert rc.count("is-title") == 1 # 합계 줄엔 안 붙음
|
||||
assert "white-space-collapse: preserve-spaces;" in r["css"]
|
||||
assert "tr.is-title td" in r["css"]
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
background: var(--color-canvas);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
white-space-collapse: preserve-spaces; /* 앞 빈칸(들여쓰기) · 겹 빈칸을 접지 않음 · 줄바꿈은 안 함 */
|
||||
}
|
||||
|
||||
/* 숫자 — 오른쪽 맞춤 · 자리 폭 같게 */
|
||||
@@ -134,6 +135,12 @@
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 제목 줄(PLAN 155-2 · rowClass) — 굵게 */
|
||||
.ui-data-table__table tbody tr.is-title td {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.ui-data-table__table tbody tr.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 열 = { key: "cut_soil_area", label: "단면적", group: ["절토", "토사"], kind: "number", decimals: 2 }
|
||||
* 고칠 칸(PLAN 121) = editable(줄, 열) 참인 칸만 글 상자 · 바꾸면 onEdit(줄, 열, 글) — 다시 그려도 커서 자리 지킴
|
||||
* 칸 누름(PLAN 144) = onCellClick(줄, 열) · 줄로 이동 = t.focusRow(줄)(스크롤 + 강조)
|
||||
* 줄 꼴(PLAN 155-2) = rowClass(줄) → 줄 class(예 is-title 굵게) · 안 주면 글 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_data_table.css";
|
||||
@@ -57,6 +58,8 @@ export interface DataTableOptions {
|
||||
editable?: (index: number, key: string) => boolean;
|
||||
/** 고친 칸 글(바꾼 뒤 · 커서가 옮겨 간 다음에 부름) */
|
||||
onEdit?: (index: number, key: string, text: string) => void;
|
||||
/** 줄 꼴 class(본문 줄만 · 예 `is-title` = 굵게) — 빈 값이면 안 붙임 */
|
||||
rowClass?: (index: number) => string | null | undefined;
|
||||
}
|
||||
|
||||
export interface DataTable {
|
||||
@@ -135,6 +138,7 @@ function rowHtml(
|
||||
edit: number | null = null,
|
||||
editable?: (index: number, key: string) => boolean,
|
||||
focus = false,
|
||||
extra: string | null | undefined = null,
|
||||
): string {
|
||||
let tr = "";
|
||||
for (let j = 0; j < cols.length; j++) {
|
||||
@@ -157,11 +161,13 @@ function rowHtml(
|
||||
(fmts[j] ? ' data-num=""' : "");
|
||||
tr += `<td${attrs}>${esc(txt)}</td>`;
|
||||
}
|
||||
const cls = [alt && "is-alt", index !== null && "is-clickable", focus && "is-focus"]
|
||||
const cls = [alt && "is-alt", index !== null && "is-clickable", focus && "is-focus", extra]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const data = index !== null ? ` data-i="${index}"` : "";
|
||||
return cls || data ? `<tr${cls ? ` class="${cls}"` : ""}${data}>${tr}</tr>` : `<tr>${tr}</tr>`;
|
||||
return cls || data
|
||||
? `<tr${cls ? ` class="${esc(cls)}"` : ""}${data}>${tr}</tr>`
|
||||
: `<tr>${tr}</tr>`;
|
||||
}
|
||||
|
||||
/** 그릴 줄 구간 — 밖의 줄은 빈 줄 높이(rowH px)로만 채움 */
|
||||
@@ -211,6 +217,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
|
||||
i,
|
||||
o.editable,
|
||||
o.focus === i,
|
||||
o.rowClass?.(i),
|
||||
);
|
||||
if (win) body += pad((n - to) * win.rowH, cols.length);
|
||||
const foot = o.total ? `<tfoot>${rowHtml(cols, fmts, left, o.total, false, blank)}</tfoot>` : "";
|
||||
|
||||
Reference in New Issue
Block a user