"""236-1 B08 · B09 표 아래 안내 글 — 제작 · 셈 설명 지움 · 늘 한 줄 · 페이지 스크롤 0. - 안내 글 낱말에 제작 · 셈 설명(ROUND · 「 = 」 · 장 번호 · M01 · 곱셈 나눗셈 기호)이 없음 - 안내 글 한 조각 70 자 이하(긴 글은 줄임표 + 공용 호버) - B08 안내 줄(footNote) = 한 줄(nowrap · 줄임표) · B09 안내 글 = 203 줄 수 변수만큼(test_203) - 창 크기(1080 · 768 · 600)마다 한 줄 안내 + 표 칸 = 창 끝(199-1 fillViewport 셈) """ import json import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] B08 = ROOT / "B08_Quantity" B09 = ROOT / "B09_Estimation" STRING = re.compile(r'"([^"\\\n]*)"|`([^`\n]*)`') FORBID = ["ROUND", " = ", "÷", "×", "Σ", "M01", "역전개", "원문"] CHAPTER = re.compile(r"\(\d{2,3}-\d\)|\b\d{1,3}-\d-\d\b|\b\d{2,3}-\d\b") MAX_PIECE = 70 def _read(path: Path) -> str: return path.read_text("utf-8") def _pieces(expr: str) -> list[str]: out = [] for m in STRING.finditer(expr): text = m.group(1) if m.group(1) is not None else m.group(2) out.append(re.sub(r"\$\{[^}]*\}", "", text).strip()) return [p for p in out if p] def _b09_hints() -> list[tuple[str, str]]: found: list[tuple[str, str]] = [] for path in sorted(B09.glob("B09_Estimation_UI_Tabs_*.ts")): src = _read(path) for m in re.finditer(r"\bhint:\s*(.*?),\n\s+\w+[:(]", src, re.S): found += [(path.name, p) for p in _pieces(m.group(1))] for m in re.finditer( r"^const (?:LINK_HINT|BLANK|HINT|EXPENSE_NONE|GUIDE)\s*=\s*(.*?);\n", src, re.S | re.M ): found += [(path.name, p) for p in _pieces(m.group(1))] return found def _b08_hints() -> list[tuple[str, str]]: found: list[tuple[str, str]] = [] for name in ( "B08_Quantity_UI_Earthwork.ts", "B08_Quantity_UI_Earthwork_Sheets.ts", "B08_Quantity_UI_Material.ts", ): src = _read(B08 / name) for m in re.finditer(r"footNote\(((?:\s|\+|\"[^\"\n]*\"|`[^`\n]*`)+)\)", src): found += [(name, p) for p in _pieces(m.group(1))] for m in re.finditer(r"^const HINTS[^=]*=\s*\{(.*?)\n\};", src, re.S | re.M): found += [(name, p) for p in _pieces(m.group(1))] for m in re.finditer(r"^const HIDDEN\s*=\s*(.*?);\n", src, re.S | re.M): found += [(name, p) for p in _pieces(m.group(1))] return found def _server_notes() -> list[tuple[str, str]]: found: list[tuple[str, str]] = [] for name in ("Router_Summary", "Router_Material"): src = _read(B08 / f"B08_Quantity_{name}.py") for m in re.finditer(r"notes\.append\(\s*(.*?)\)\n", src, re.S): found += [(name, p) for p in _pieces(m.group(1).replace('f"', '"'))] sangeun = _read(B09 / "B09_Estimation_Sangeun.py") for m in re.finditer(r'^(?:NO_SOURCE|EMPTY) = "([^"]*)"', sangeun, re.M): found.append(("Sangeun", m.group(1))) return found def _all() -> list[tuple[str, str]]: return _b09_hints() + _b08_hints() + _server_notes() def test_안내_글_모음이_비지_않음(): assert len(_b09_hints()) >= 20 assert len(_b08_hints()) >= 4 assert len(_server_notes()) >= 7 def test_제작_셈_설명_낱말_없음(): bad = [] for where, text in _all(): bad += [(where, token, text) for token in FORBID if token in text] if CHAPTER.search(text): bad.append((where, "장 번호", text)) assert not bad, bad def test_안내_글_조각_짧음(): long = [(w, t) for w, t in _all() if len(t) > MAX_PIECE] assert not long, long # ── 늘 한 줄 ─────────────────────────────────────────────────────────────── 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_B09_안내_글_줄_수_변수(): css = _read(B09 / "B09_Estimation_UI_Page.css") block = _block(css, ".b09-estimation__line-hint") # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤(test_203_b09_hint_lines) assert "max-height: calc(var(--b09-hint-lines) * 1lh)" in block and "overflow-y: auto" in block shell = _read(B09 / "B09_Estimation_UI_Shell.ts") assert "lineHint(note(spec.hint))" in shell # 표 탭도 묶음 탭과 같은 꼴 assert "export function lineHint" in _read(B09 / "B09_Estimation_UI_Tabs_Groups.ts") level = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts") assert 'guide.className = "ui-general-block__empty b09-estimation__line-hint"' in level def test_B08_안내_줄_한_줄(): css = _read(B08 / "B08_Quantity_UI_Page.css") block = _block(css, ".b08-quantity__foot") assert "white-space: nowrap" in block and "text-overflow: ellipsis" in block assert "display" not in block # hidden 속성(콘크리트 빈 까닭)을 덮지 않음 foot = _read(B08 / "B08_Quantity_UI_Foot.ts") assert "b08-quantity__foot" in foot and "attachHoverInfo" in foot for name in ("Earthwork", "Earthwork_Sheets", "Material", "Summary"): assert "footNote" in _read(B08 / f"B08_Quantity_UI_{name}.ts"), name def test_B08_표_아래_안내는_footNote만(): material = _read(B08 / "B08_Quantity_UI_Material.ts") assert "note(BASIS)" not in material # 「구조물 수량 기준 …」 제작 설명 줄 없음 assert "const excel = footNote(" in material assert "const concreteNote = footNote(" in material summary = _read(B08 / "B08_Quantity_UI_Summary.ts") assert 'footNote(summary.notes.join(" · "))' in summary # 서버 알림 = 한 줄로 이어 붙임 sheets = _read(B08 / "B08_Quantity_UI_Earthwork_Sheets.ts") assert ( "tableCard(haulNote(table), grid.root, averages.map(note))" in sheets ) # 제작 설명 줄 없음 # ── 창 끝까지 셈 ──────────────────────────────────────────────────────────── def test_창_크기마다_한_줄_안내_뒤_창_끝(): proc = subprocess.run( ["node", str(ROOT / "resources" / "tester" / "helper_236_1_fill_sizes.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr rows = json.loads(proc.stdout) assert [r["viewport"] for r in rows] == [1080, 768, 600] for r in rows: assert r["below"] == 18 + 8 + 12 + 8 # 안내 한 줄 + 줄 사이 + 카드 + 본문 여백 assert r["bottom"] == r["viewport"], r # 문서 맨 아래 = 창 끝 → 페이지 스크롤 0