diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index 9f9a4b97a..27be4e313 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -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); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts index 0bc6e9f87..666f0ea42 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts @@ -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(); -/** 묶음 탭 — 카드 = 안내 + 카드 판(보기 단추는 가로 열 수 줄) · 표 = 머리(안내 + 보기 단추) + 표 */ +/** 묶음 탭 — 카드 = 카드 판(안내 · 가로 열 수 · 보기 단추 한 줄) · 표 = 머리(안내 + 보기 단추 한 줄) + 표 */ 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)); diff --git a/resources/tester/helper_b09_group_cards.py b/resources/tester/helper_b09_group_cards.py index 45027fdd4..11887582b 100644 --- a/resources/tester/helper_b09_group_cards.py +++ b/resources/tester/helper_b09_group_cards.py @@ -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""" diff --git a/resources/tester/test_185_2_b09_view_bar.py b/resources/tester/test_185_2_b09_view_bar.py index 7ddc65174..f28188e2d 100644 --- a/resources/tester/test_185_2_b09_view_bar.py +++ b/resources/tester/test_185_2_b09_view_bar.py @@ -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" diff --git a/resources/tester/test_200_1_b09_box_tool_line.py b/resources/tester/test_200_1_b09_box_tool_line.py new file mode 100644 index 000000000..57d9e08fc --- /dev/null +++ b/resources/tester/test_200_1_b09_box_tool_line.py @@ -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 diff --git a/ui_template/ui_template_group_cards.css b/ui_template/ui_template_group_cards.css index 58499235f..ed4f4f584 100644 --- a/ui_template/ui_template_group_cards.css +++ b/ui_template/ui_template_group_cards.css @@ -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; } diff --git a/ui_template/ui_template_group_cards.ts b/ui_template/ui_template_group_cards.ts index 65923f767..4eb6cc64b 100644 --- a/ui_template/ui_template_group_cards.ts +++ b/ui_template/ui_template_group_cards.ts @@ -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 ?? []));