feat(B09): 박스보기 도구 줄 한 줄 · 안내 글 줄임표 + 호버 · 카드 같은 가로 줄 높이 같게(200-1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
This commit is contained in:
2026-10-10 14:13:18 +09:00
co-authored by Claude Sonnet 5.5
parent 58db0dab7f
commit 8f169f5fe2
7 changed files with 163 additions and 8 deletions
+16
View File
@@ -50,6 +50,22 @@
white-space: nowrap;
}
/* 200-1 묶음 탭 도구 줄 — 안내 글 = 남는 자리 한 줄 · 넘치면 줄임표(전문 = 공용 호버) */
.b09-estimation__bar {
flex-wrap: nowrap;
align-items: center;
}
.b09-estimation__line-hint {
flex: 1 1 0;
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--text-caption);
}
/* 182 설계내역서 레벨 — 글자 크기(줄 높이 28px 고정 → 16px 까지) · 첫 칸 들여쓰기 1em × (레벨 − 1) */
.b09-estimation .ui-data-table__table tbody tr.is-lv1 td {
font-size: var(--text-body);
@@ -7,10 +7,12 @@
* 머리 줄 = 묶음 첫 줄 · 합계 줄 = 키 끝 `|합계` · `|총계` · 그 밖 = 몸 줄(표 줄 번호 그대로)
* 목록표에서 온 호표(181-2) = 박스 붉은 외곽선 · 표 보기 = 그 호표 줄 구간 전체 붉은 외곽선
* 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2
* 도구 줄(200-1) = 안내 글 + 가로 열 수 + 보기 단추 한 줄 · 안내 글 길면 줄임표 + 공용 호버로 전문
* ========================================================================== */
import type { DataRow, DataTable } from "@ui/ui_template_data_table";
import { createButton } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import {
clampColumns,
createGroupCards,
@@ -174,10 +176,18 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine
/* ── 그리기 ───────────────────────────────────────────────────────────── */
/** 안내 글 = 도구 줄 한 칸(줄임표) · 잘렸을 때만 마우스 올리면 전문(공용 호버 185-3) */
function lineHint(hint: HTMLElement | null): HTMLElement | null {
if (!hint) return null;
hint.classList.add("b09-estimation__line-hint");
attachHoverInfo(hint, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null));
return hint;
}
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
const chosen = new Map<TabId, GroupView>();
/** 묶음 탭 — 카드 = 안내 + 카드 판(보기 단추는 가로 열 수 줄) · 표 = 머리(안내 + 보기 단추) + 표 */
/** 묶음 탭 — 카드 = 카드 판(안내 · 가로 열 수 · 보기 단추 한 줄) · 표 = 머리(안내 + 보기 단추 한 줄) + 표 */
export function renderGroupPanel(o: {
tab: TabId;
spec: TabTable;
@@ -188,6 +198,7 @@ export function renderGroupPanel(o: {
redraw: () => void;
}): HTMLElement[] {
const view = chosen.get(o.tab) ?? readGroupView(o.tab);
const lead = lineHint(o.hint);
const toggle = createButton({
label: viewLabel(view),
variant: "ghost",
@@ -210,14 +221,15 @@ export function renderGroupPanel(o: {
focus: at,
money: numberFormat(o.spec, "sum"),
qty: numberFormat(o.spec, "qty"),
lead, // 200-1 안내 글도 같은 줄
actions: [toggle], // 185-2 가로 열 수 단추와 한 줄
});
cards.dataset.b09Table = o.tab;
return [...(o.hint ? [o.hint] : []), cards];
return [cards];
}
const bar = document.createElement("div");
bar.className = "b09-estimation__bar";
bar.append(...(o.hint ? [o.hint] : []), toggle);
bar.append(...(lead ? [lead] : []), toggle);
const range = pickRange(o.spec.rows, o.focus);
const table = o.makeTable((i) => pickClass(range, i));
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
+3 -1
View File
@@ -15,7 +15,7 @@ COMMON = "ui_template/ui_template_group_cards.ts"
TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts"
GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts"
# 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 157-3 원문번호 풍선(raw) 짝
# 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
STUBS = r"""
const kept = new Map();
const readState = (name) => (kept.has(name) ? JSON.parse(kept.get(name)) : null);
@@ -27,6 +27,8 @@ const createButton = (o) => {
if (o.onClick) b.addEventListener("click", o.onClick);
return b;
};
const hoverCalls = [];
const attachHoverInfo = (root, text, selector) => { hoverCalls.push({ root, text, selector }); return () => {}; };
"""
RAW = r"""
+3 -2
View File
@@ -51,9 +51,10 @@ def test_카드_보기_단추가_가로_열수_줄에():
assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐
def test_카드_보기_안내_글은_카드_위에_따로():
def test_카드_보기_안내_글은_같은_도구_줄에():
# 200-1 — 안내 글이 카드 위 따로 줄에서 도구 줄 안으로(상세 = test_200_1)
out = _panel("cards", HINT)
assert out["n"] == 2 and out["first"] == "hint"
assert out["n"] == 1 and out["first"] == "ui-group-cards"
assert out["bar"] == "ui-group-cards__bar"
@@ -0,0 +1,118 @@
"""200-1 B09 박스보기 도구 줄 한 줄 · 카드 같은 가로 줄 높이 같게.
- 안내 글 · 가로 열 수 단추 · 보기 단추 = 같은 줄(카드 보기 = `ui-group-cards__bar` · 표 보기 = `b09-estimation__bar`)
- 안내 글 = 줄임표 한 줄 · 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문
- 카드 격자 = 행 늘림(`align-items: stretch`) · 카드 = 세로 flex · 몸통이 늘어 합계 줄이 아래
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402
B09 = "B09_Estimation/B09_Estimation_UI_Tabs_"
DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0}
LONG = "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가"
HINT = (
"(() => { const h = document.createElement('p');"
f" h.className = 'ui-general-block__empty'; h.textContent = '{LONG}'; return h; }})()"
)
def _block(css: str, selector: str) -> str:
m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css)
assert m, selector
return m.group(1)
def _panel(view: str, hint: str):
"""고른 보기 · 안내 글로 그린 뒤 도구 줄 구성 · 호버 기록."""
return run(
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
f"\nchosen.set('dg', '{view}');"
f"\nconst hint = {hint};"
"\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {},"
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
"\nconst out = renderGroupPanel(o);"
"\nconst btn = out.flatMap((e) => (e.dataset.b09GroupView ? [e] : e.all('[data-b09-group-view]')))[0];"
"\nconst bar = btn.closest('.ui-group-cards__bar') ?? btn.closest('.b09-estimation__bar');"
"\nconst h = hint ? bar.all('.b09-estimation__line-hint')[0] : null;"
"\nconst call = hoverCalls[0];"
"\nconst wide = call ? call.text({ scrollWidth: 900, clientWidth: 300, textContent: 'T' }) : null;"
"\nconst fit = call ? call.text({ scrollWidth: 300, clientWidth: 300, textContent: 'T' }) : null;"
"\nconsole.log(JSON.stringify({ n: out.length, bar: bar.className,"
" cols: bar.all('[data-group-cols]').length, hint: !!h, hintInBar: h ? bar.contains(h) : false,"
" hintCls: h ? h.className : '', hover: hoverCalls.length, hoverIsHint: call ? call.root === h : false,"
" wide, fit, outer: out.map((e) => e.className) }));",
RAW,
)
def test_카드_보기_안내_글이_단추와_같은_줄():
out = _panel("cards", HINT)
assert out["n"] == 1 # 안내 글 따로 줄 없음 · 카드 판 하나
assert out["bar"] == "ui-group-cards__bar"
assert out["hint"] and out["hintInBar"] # 열 수 단추 · 보기 단추와 같은 도구 줄 안
assert out["cols"] == 4
assert "b09-estimation__line-hint" in out["hintCls"]
def test_표_보기_안내_글이_보기_단추와_같은_줄():
out = _panel("table", HINT)
assert out["n"] == 2 # 머리 줄 + 표
assert out["bar"] == "b09-estimation__bar"
assert out["hint"] and out["hintInBar"]
def test_안내_글_잘렸을_때만_공용_호버로_전문():
for view in ("cards", "table"):
out = _panel(view, HINT)
assert out["hover"] == 1 and out["hoverIsHint"]
assert out["wide"] == "T" # 잘림(scrollWidth > clientWidth) = 전문
assert out["fit"] is None # 다 보이면 안 띄움
def test_안내_글_없으면_호버도_없음():
for view in ("cards", "table"):
out = _panel(view, "null")
assert out["hint"] is False and out["hover"] == 0
def test_CSS_안내_글_줄임표_한_줄():
page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
hint = _block(page, ".b09-estimation__line-hint")
for need in (
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
"min-width: 0",
):
assert need in hint, need
assert "flex: 1 1 0" in hint
# 표 보기 머리 줄 = 줄바꿈 없음 · 같은 높이 가운데
bars = re.findall(r"\.b09-estimation__bar\s*\{([^}]*)\}", page)
assert any("flex-wrap: nowrap" in b and "align-items: center" in b for b in bars)
def test_CSS_카드_같은_줄_높이_같게():
css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8")
grid = _block(css, ".ui-group-cards__grid")
assert "align-items: stretch" in grid and "align-items: start" not in grid
card = _block(css, ".ui-group-card")
assert "display: flex" in card and "flex-direction: column" in card
assert "flex: 1 1 auto" in _block(css, ".ui-group-card__body") # 합계 줄 = 카드 아래
def test_잇기():
groups = (ROOT / GROUPS).read_text("utf-8")
assert "lead," in groups and "actions: [toggle]" in groups
assert "attachHoverInfo(hint," in groups
cards = (ROOT / COMMON).read_text("utf-8")
assert "if (opts.lead) bar.append(opts.lead);" in cards
+4 -1
View File
@@ -43,11 +43,13 @@
.ui-group-cards__grid {
display: grid;
grid-template-columns: repeat(var(--group-cols, 2), minmax(0, 1fr));
align-items: start;
align-items: stretch; /* 200-1 같은 가로 줄 = 높이 같음 */
gap: var(--spacing-8);
}
.ui-group-card {
display: flex;
flex-direction: column;
min-width: 0;
padding: var(--spacing-4) var(--spacing-8);
background-color: var(--color-surface-raised);
@@ -87,6 +89,7 @@
}
.ui-group-card__body {
flex: 1 1 auto; /* 내용 적은 카드도 몸통이 늘어 합계 줄이 아래에 */
overflow-x: auto;
}
+4 -1
View File
@@ -5,7 +5,7 @@
* 머리 = 호표 · 명칭 · 규격 · 단위 · 합계 / 몸 = 줄(이름 · 규격 · 수량 · 단위 · 합계 · 비고) /
* 합계 줄 = 노무 · 재료 · 경비 · 계 · 줄 금액 = 합계만(비목 셋 = 풍선)
* 글자 14px 한 벌 · 줄 간격 좁게 · 숫자 오른쪽 · 가로 열 수 1~4(기억은 부르는 쪽)
* 접기 없음 — 묶음 모두 펼친 박스
* 접기 없음 — 묶음 모두 펼친 박스 · 같은 가로 줄 카드 = 높이 같음(200-1 · 합계 줄은 아래 붙음)
* 줄 번호(`at`) = 부르는 쪽 표 줄 번호 — 누름 = `onPick(at)` · 이동해 온 줄 = 그 박스 붉은 외곽선
* (`is-pick`) + 스크롤 + 그 줄 강조(`is-focus`)
* ========================================================================== */
@@ -65,6 +65,8 @@ export interface GroupCardsOptions {
focus?: number;
money?: (value: number | null | undefined) => string;
qty?: (value: number | null | undefined) => string;
/** 머리 띠 가운데 안내 글(200-1 · 단추와 한 줄 — 늘어나는 칸) */
lead?: HTMLElement | null;
/** 머리 띠 끝 단추(예 [표로 보기]) */
actions?: HTMLElement[];
}
@@ -209,6 +211,7 @@ export function createGroupCards(opts: GroupCardsOptions): HTMLElement {
});
pick.append(...colButtons);
bar.append(pick, span("ui-group-cards__count", `묶음 ${opts.groups.length}`));
if (opts.lead) bar.append(opts.lead);
const tail = document.createElement("div");
tail.className = "ui-group-cards__actions";
tail.append(...(opts.actions ?? []));