diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index fe87eb951..2a2041307 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -1,6 +1,7 @@ /* B09 원가계산 — 탭 줄 · 안내 글. 색은 테마 변수만(밝음 · 어두움 같이). */ .b09-estimation { + --b09-hint-lines: 3; /* 203 안내 글 영역 줄 수 — 이 값만 바꿈(높이 · 스크롤 · 표 칸 같이 맞음) */ display: flex; flex-direction: column; gap: var(--spacing-8); @@ -57,7 +58,7 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child { white-space: nowrap; } -/* 202-1 안내 줄 = 표 · 카드 판 아래(묶음 탭은 한 줄 줄임표 · 전문 = 공용 호버) */ +/* 202-1 안내 줄 = 표 · 카드 판 아래 · 203 줄 수 변수만큼 보임 · 넘치면 영역 안 스크롤 */ .b09-estimation .ui-data-table + .ui-general-block__empty, .b09-estimation .ui-group-cards + .ui-general-block__empty { margin: var(--spacing-8) 0 0; @@ -65,9 +66,8 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child { .b09-estimation__line-hint { min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + max-height: calc(var(--b09-hint-lines) * 1lh); + overflow-y: auto; font-size: var(--text-caption); } diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index 81dc5157f..ff7aee710 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -183,6 +183,7 @@ export function renderB09Estimation(root: HTMLElement): void { const makeTable = (extra?: (index: number) => string | null): DataTable => { const made = createDataTable({ ...tableOptionsOf(spec, tab), + fillViewport: true, // 203 표 칸 = 창 끝 − 안내 글 영역(팝업 · 인쇄는 그대로) rowClass: extra ? (i) => [spec.rowClass?.(i), extra(i)].filter(Boolean).join(" ") || null : spec.rowClass, @@ -230,7 +231,7 @@ export function renderB09Estimation(root: HTMLElement): void { ); return made; }; - const hint = spec.hint ? lineHint(note(spec.hint)) : null; // 236-1 안내 글 = 한 줄 + const hint = spec.hint ? lineHint(note(spec.hint)) : null; // 203 안내 글 = 줄 수 변수만큼 · 넘치면 스크롤 // 171-3 묶음 탭(일위대가표 · 중기사용료 · 단가산출근거) = 묶음 카드 기본 · 좌측 하단 스위치로 표(304-1) if (isGroupTab(tab)) { return void panel.replaceChildren( @@ -259,7 +260,10 @@ export function renderB09Estimation(root: HTMLElement): void { if (range) unpickOnRelease(table.root); const level = bill ? levelBar(table.root, bill, (i) => table.edit(i)) : null; if (level) bars.setTools(level.tools); - panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : [])); + // 203 안내 글 영역 하나 — 설계내역서 레벨 안내 + 탭 안내 이어 붙임 + if (level && hint) hint.textContent = `${level.guide.textContent} · ${hint.textContent}`; + const foot = hint ?? level?.guide; + panel.replaceChildren(table.root, ...(foot ? [foot] : [])); if (at >= 0) requestAnimationFrame(() => table.focusRow(at, false)); // 스크롤만(표시 = 빨강) }; diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts index 5ffe20bb2..1cc4f2af6 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts @@ -228,7 +228,7 @@ function numberFormat(spec: TabTable, key: string): (v: number | null | undefine /* ── 그리기 ───────────────────────────────────────────────────────────── */ -/** 안내 글 = 한 줄(줄임표) · 잘렸을 때만 마우스 올리면 전문(공용 호버 185-3) */ +/** 안내 글 = 줄 수 변수(203 `--b09-hint-lines`)만큼 · 넘치면 영역 안 스크롤 · 가로로 잘렸을 때만 전문(공용 호버 185-3) */ export function lineHint(hint: HTMLElement | null): HTMLElement | null { if (!hint || hint.classList.contains("b09-estimation__line-hint")) return hint; hint.classList.add("b09-estimation__line-hint"); diff --git a/resources/tester/test_200_1_b09_box_tool_line.py b/resources/tester/test_200_1_b09_box_tool_line.py index 203d08e9b..b0c95cb85 100644 --- a/resources/tester/test_200_1_b09_box_tool_line.py +++ b/resources/tester/test_200_1_b09_box_tool_line.py @@ -1,6 +1,6 @@ """200-1 B09 박스보기 안내 글 줄임표 + 호버 · 카드 같은 가로 줄 높이 같게. -- 안내 글 = 줄임표 한 줄 · 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문 · 자리 = 표 · 카드 판 아래(202-1 로 옮김) +- 안내 글 = 203 줄 수 변수만큼(넘치면 스크롤) · 가로로 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문 · 자리 = 표 · 카드 판 아래(202-1 로 옮김) - 카드 격자 = 행 늘림(`align-items: stretch`) · 카드 = 세로 flex · 몸통이 늘어 합계 줄이 아래 - 카드 제목 줄 · 합계 줄 바탕색(200-3) """ @@ -78,13 +78,12 @@ def test_안내_글_없으면_호버도_없음(): assert out["n"] == 1 and out["hintLast"] is False and out["hover"] == 0 -def test_CSS_안내_글_줄임표_한_줄(): +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", + for need in ( # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤 + "max-height: calc(var(--b09-hint-lines) * 1lh)", + "overflow-y: auto", "min-width: 0", ): assert need in hint, need 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 8da7177e0..2aab73e04 100644 --- a/resources/tester/test_200_2_b09_bill_level_bar.py +++ b/resources/tester/test_200_2_b09_bill_level_bar.py @@ -200,7 +200,9 @@ def test_셸_배선(): "bill ? (i) => pickedClass(bill.rows[i]) ?? pickClass(jumpRange(), i)" in src ) # 239-1 · 242-1 이동 줄 빨강 곁(고름과 한때 하나) assert "if (level) bars.setTools(level.tools);" in src # 229-1 단추 = 보조 탭 줄 - assert "panel.replaceChildren(table.root, ...(level ? [level.guide] : [])" in src + assert ( + "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in src + ) # 203 레벨 안내 + 탭 안내 = 한 영역 lvl = (B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8") assert "table.menu(index)" in lvl # 단추 · 키 = 우클릭 메뉴 같은 함수 assert "b09-estimation__level" not in lvl diff --git a/resources/tester/test_202_1_table_hint_bottom.py b/resources/tester/test_202_1_table_hint_bottom.py index 8cbe24bb4..ff5509be6 100644 --- a/resources/tester/test_202_1_table_hint_bottom.py +++ b/resources/tester/test_202_1_table_hint_bottom.py @@ -106,9 +106,8 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에(): def test_잇기_쉘_안내_줄_아래_도구_비움(): shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") assert ( - "panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : []))" - in shell - ) # 229-1 레벨 단추 = 보조 탭 줄 · 안내 글 = 표 아래 · 안내 줄 = 맨 아래 + "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell + ) # 229-1 레벨 단추 = 보조 탭 줄 · 안내 글 = 표 아래 · 203 레벨 안내 + 탭 안내 = 한 영역(맨 아래) assert "...(hint ? [hint] : []), table.root" not in shell # 위 자리 없음 assert re.search(r"bars\.draw\(tab\);\s*bars\.setTools\(\[\]\);", shell) assert "setTools: bars.setTools" in shell diff --git a/resources/tester/test_203_b09_hint_lines.py b/resources/tester/test_203_b09_hint_lines.py new file mode 100644 index 000000000..7d58bdf37 --- /dev/null +++ b/resources/tester/test_203_b09_hint_lines.py @@ -0,0 +1,185 @@ +"""203 B09 탭 상세 안내 글 영역 = 줄 수 변수(`--b09-hint-lines` 기본 3)만큼 · 넘치면 영역 안 스크롤 · 표 칸 = 창 끝까지. + +- CSS: 줄 수 변수 하나 · 안내 글 최대 높이 = 변수 × 줄 높이(1lh) · 세로 스크롤 · 한 줄 줄임표 걷음 +- Shell: 탭 표 = 공용 fillViewport(팝업 · 인쇄 표는 그대로) · 설계내역서 레벨 안내 + 탭 안내 = 한 영역 +- 공용 spaceBelow: 아래 형제 바깥 여백(B09 안내 글 margin-top)도 셈 — 빼면 페이지 스크롤이 생김 +- 실제 브라우저(정적 쪽 · 서버 불필요): 변수 3 · 2 × 줄 많음 · 적음 — 안내 글 높이 · 합계 줄 · 안내 글 자리 · 페이지 스크롤 0 +""" + +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +B09 = ROOT / "B09_Estimation" +UI = ROOT / "ui_template" + + +def _read(path: Path) -> str: + return path.read_text("utf-8") + + +def _block(css: str, selector: str) -> str: + m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) + assert m, selector + return m.group(1) + + +def test_CSS_줄_수_변수_하나(): + css = _read(B09 / "B09_Estimation_UI_Page.css") + assert re.search(r"--b09-hint-lines:\s*3;", _block(css, ".b09-estimation")) + assert css.count("--b09-hint-lines:") == 1 # 고칠 자리 하나 + hint = _block(css, ".b09-estimation__line-hint") + assert "max-height: calc(var(--b09-hint-lines) * 1lh)" in hint + assert "overflow-y: auto" in hint + for gone in ("white-space: nowrap", "text-overflow: ellipsis"): + assert gone not in hint, gone + + +def test_Shell_표_창_끝_안내_한_영역(): + shell = _read(B09 / "B09_Estimation_UI_Shell.ts") + assert "fillViewport: true" in shell + assert "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell + assert "hint.textContent = `${level.guide.textContent} · ${hint.textContent}`" in shell + # 팝업 · 인쇄 표는 창 끝 셈 안 씀(팝업 = 창 안 max-height · 인쇄 = 펼침) + assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Builders.ts") + assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Popup.ts") + + +# ── 실제 브라우저 ───────────────────────────────────────────────────────────── + +TOKENS = ( + ":root { --spacing-4: 4px; --spacing-8: 8px; --spacing-16: 16px; --spacing-24: 24px;" + " --text-caption: 12px; --radius-lg: 8px; }" + " html, body { margin: 0; line-height: 1.5; font-family: sans-serif; }" +) +LONG = " · ".join(["긴 안내 글 시험 문장"] * 40) +SHORT = "줄을 누르면 단가를 낸 탭 · 줄로 이동" + + +def _module_js() -> str: + """공용 표 모듈(TS) → 브라우저 ES 모듈(CSS import 뺌).""" + src = UI / "ui_template_data_table.ts" + code = ( + "const ts = require(process.argv[1]);" + "const fs = require('fs');" + "const out = ts.transpileModule(fs.readFileSync(process.argv[2], 'utf8')," + " { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 } });" + "process.stdout.write(out.outputText);" + ) + ts_path = ROOT / "config" / "node_modules" / "typescript" + proc = subprocess.run( + ["node", "-e", code, str(ts_path), str(src)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return re.sub(r'^import\s+"[^"]+\.css";\s*$', "", proc.stdout, flags=re.M) + + +MEASURE = """async ([rows, lines, text]) => { + const { createDataTable } = window.dt; + const body = document.querySelector('.b09-estimation'); + body.style.setProperty('--b09-hint-lines', String(lines)); + const panel = body.querySelector('.b09-estimation__panel'); + const table = createDataTable({ + columns: [{ key: 'name', label: '이름' }, { key: 'sum', label: '금액', kind: 'number' }], + rows: Array.from({ length: rows }, (_, i) => ({ name: '줄 ' + i, sum: i })), + total: { name: '계', sum: 1 }, + fillViewport: true, + }); + const hint = document.createElement('p'); + hint.className = 'ui-general-block__empty b09-estimation__line-hint'; + hint.textContent = text; + panel.replaceChildren(table.root, hint); + await new Promise((r) => setTimeout(r, 300)); + const t = table.root.getBoundingClientRect(); + const h = hint.getBoundingClientRect(); + const foot = table.root.querySelector('tfoot').getBoundingClientRect(); + return { + vh: innerHeight, + page: document.scrollingElement.scrollHeight, + lh: parseFloat(getComputedStyle(hint).lineHeight), + hintH: h.height, + hintScrolls: hint.scrollHeight > hint.clientHeight, + tableTop: t.top, + tableBottom: t.bottom, + tableScrolls: table.root.scrollHeight > table.root.clientHeight, + maxH: parseFloat(table.root.style.maxHeight), + hintTop: h.top, + hintBottom: h.bottom, + padB: parseFloat(getComputedStyle(body).paddingBottom), + footBottom: foot.bottom, + }; +}""" + + +@pytest.fixture(scope="module") +def page(): + from playwright.sync_api import sync_playwright + + css = _read(UI / "ui_template_data_table.css") + _read(B09 / "B09_Estimation_UI_Page.css") + html = ( + f"
" + "