diff --git a/resources/tester/bench_342_watershed.py b/resources/tester/bench_342_watershed.py
deleted file mode 100644
index ef0653783..000000000
--- a/resources/tester/bench_342_watershed.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""342-3 · 347-1 배수유역 빠른 길 실측 — 검증_sub3 도엽 · 노선으로 preview_stages 를 돌려
-시간과 결과(적색 · 흐름 방향 · 도로 귀속 · 강도)를 tmp/342/<이름>.npz 에 남기고 기준과 대조한다.
-
- ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py base # 기준 저장
- ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py new base # 새로 돌려 대조
- ./venv/Scripts/python.exe resources/tester/bench_342_watershed.py s1 base store # 흐름 방향 저장본
-
-pytest 대상 아님(DB · 프로젝트 파일이 있어야 돎). 분석 결과는 저장 안 함 — 도엽 좌표 저장본
-(drainage/00_sheet_metric.pkl)만 프로젝트에 남고 · 흐름 방향 저장본은 tmp/342/descent 에 둠.
-"""
-
-import asyncio
-import json
-import logging
-import sys
-import time
-from pathlib import Path
-from uuid import UUID
-
-import numpy as np
-
-ROOT = Path(__file__).resolve().parents[2]
-sys.path.insert(0, str(ROOT))
-PID = UUID("1f93a515-df0a-4975-a8ba-daaa7672b123") # 검증_sub3
-OUT_DIR = ROOT / "tmp" / "342"
-
-
-async def _prepare():
- from config.config_db import close_db_pool, init_db_pool
-
- await init_db_pool()
- try:
- from B04_PreProcess.B04_PreProcess_Router_Watershed import _prepare as prepare
-
- started = time.perf_counter()
- prepared = await prepare(PID)
- return prepared, time.perf_counter() - started
- finally:
- await close_db_pool()
-
-
-def main() -> None:
- name = sys.argv[1]
- base = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] != "-" else None
- store = len(sys.argv) > 3 and sys.argv[3] == "store"
- OUT_DIR.mkdir(parents=True, exist_ok=True)
- logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s %(name)s:%(message)s",
- handlers=[logging.FileHandler(OUT_DIR / f"{name}.log", "w", encoding="utf-8")],
- )
- prepared, prepare_s = asyncio.run(_prepare())
- from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import preview_stages
-
- started = time.perf_counter()
- pv = preview_stages(
- prepared["vertices"],
- prepared["contours"],
- prepared["streams"],
- (OUT_DIR / "descent", prepared["contour_key"]) if store else None,
- )
- preview_s = time.perf_counter() - started
- spec = pv.spec
- red = pv.flow.reaches_road & pv.flow.analyzed
- result = {
- "spec": np.array([spec.x_min, spec.y_max, spec.cell_m, spec.n_rows, spec.n_cols]),
- "domain": pv.domain,
- "red": red,
- "direction": pv.flow.direction,
- "road_slot": pv.routing.road_slot,
- "strength": np.array(pv.strength_profile),
- "pipes": np.array([p.chainage_m for p in pv.pipes]),
- }
- np.savez_compressed(OUT_DIR / f"{name}.npz", **result)
- report = {
- "prepare_s": round(prepare_s, 2),
- "preview_s": round(preview_s, 2),
- "total_s": round(prepare_s + preview_s, 2),
- "rounds": pv.expand_rounds,
- "grid": [spec.n_rows, spec.n_cols],
- "red_cells": int(red.sum()),
- }
- if base:
- ref = np.load(OUT_DIR / f"{base}.npz")
- same_spec = bool(np.array_equal(ref["spec"], result["spec"]))
- report["same_spec"] = same_spec
- if same_spec:
- for key in ("domain", "red", "direction", "road_slot"):
- report[f"diff_{key}"] = int((ref[key] != result[key]).sum())
- else:
- report["base_spec"] = ref["spec"].tolist()
- report["new_spec"] = result["spec"].tolist()
- report["red_cells_base"] = int(ref["red"].sum())
- report["same_strength"] = bool(np.array_equal(ref["strength"], result["strength"]))
- report["same_pipes"] = bool(np.array_equal(ref["pipes"], result["pipes"]))
- print(json.dumps(report, ensure_ascii=False))
- (OUT_DIR / f"{name}.json").write_text(json.dumps(report, ensure_ascii=False, indent=1), "utf-8")
-
-
-if __name__ == "__main__":
- main()
diff --git a/resources/tester/helper_236_1_fill_sizes.cjs b/resources/tester/helper_236_1_fill_sizes.cjs
deleted file mode 100644
index 64c240b5d..000000000
--- a/resources/tester/helper_236_1_fill_sizes.cjs
+++ /dev/null
@@ -1,72 +0,0 @@
-/* 236-1 — 표 아래 안내 글이 한 줄일 때 창 끝까지 셈(`ui_template_data_table.ts` fillViewport)이
- * 창 크기마다 문서 맨 아래 = 창 끝(페이지 스크롤 0)이 되는지 단위검증 헬퍼. JSON 으로 냄. */
-const fs = require("fs");
-const path = require("path");
-const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
-
-const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts");
-const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
-}).outputText;
-
-const ONE_LINE = 18; // 안내 글 한 줄(caption 글꼴) 높이
-const win = { innerHeight: 0, scrollY: 0, addEventListener() {}, removeEventListener() {} };
-const observers = [];
-class RO {
- constructor(fn) {
- observers.push(fn);
- }
- observe() {}
- disconnect() {}
-}
-const el = (css = {}, height = 0) => ({
- css,
- height,
- top: 0,
- parentElement: null,
- nextElementSibling: null,
- isConnected: true,
- style: {},
- getBoundingClientRect() {
- return { top: this.top, height: this.height };
- },
-});
-const sty = (n) => ({
- rowGap: n.css.rowGap ?? "normal",
- paddingBottom: n.css.paddingBottom ?? "0px",
- borderBottomWidth: n.css.borderBottomWidth ?? "0px",
- marginBottom: n.css.marginBottom ?? "0px",
-});
-const box = { exports: {} };
-new Function(
- "exports",
- "module",
- "require",
- "window",
- "document",
- "getComputedStyle",
- "ResizeObserver",
- js,
-)(box.exports, box, (p) => (p.endsWith(".css") ? {} : require(p)), win, { body: null }, sty, RO);
-
-const out = [];
-for (const h of [1080, 768, 600]) {
- win.innerHeight = h;
- observers.length = 0;
- // 문서: body(padding-bottom 8) > card(padding-bottom 12 · gap 8) > [표 칸][안내 한 줄]
- const body = el({ paddingBottom: "8px" });
- const card = el({ paddingBottom: "12px", rowGap: "8px" });
- const root = el();
- const hint = el({}, ONE_LINE);
- card.parentElement = body;
- root.parentElement = card;
- hint.parentElement = card;
- root.nextElementSibling = hint;
- root.top = 200;
- box.exports.fillViewport(root);
- observers.forEach((fn) => fn());
- const grow = parseFloat(root.style.maxHeight);
- const below = box.exports.spaceBelow(root);
- out.push({ viewport: h, bottom: root.top + grow + below, grow, below });
-}
-console.log(JSON.stringify(out));
diff --git a/resources/tester/helper_398_1_user_pick.cjs b/resources/tester/helper_398_1_user_pick.cjs
deleted file mode 100644
index 51e3d13fb..000000000
--- a/resources/tester/helper_398_1_user_pick.cjs
+++ /dev/null
@@ -1,81 +0,0 @@
-/* PLAN 398-1 단위검증 헬퍼 — 진짜 `design_user_pick.ts` + 진짜 `design_page_data.ts`(묶음 차례 · 「계산 중」)로
- * 좌측 패널 펴기 한 곳(`design_shared` onReveal 과 같은 식)을 길마다 셈. 짝 = `test_398_1_reveal_user_pick.py`.
- * 브라우저가 처리 중인 이벤트(`window.event`)는 `globalThis.event` 로 흉내. */
-const fs = require("fs");
-const path = require("path");
-const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
-
-const ROOT = path.join(__dirname, "..", "..");
-const DESIGN = path.join(ROOT, "A00_Common", "design");
-
-function load(file, stubs) {
- const js = ts.transpileModule(fs.readFileSync(path.join(DESIGN, file), "utf8"), {
- compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
- }).outputText;
- const box = { exports: {} };
- new Function("exports", "module", "require", js)(box.exports, box, (name) => {
- if (!stubs[name]) throw new Error(`stub 없음: ${name}`);
- return stubs[name];
- });
- return box.exports;
-}
-
-const data = load("design_page_data.ts", {
- "./design_page_route": { setPageRouteId: () => undefined, pageRouteId: () => null },
- "../../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({}) },
- "../../B05_Profile/B05_Profile_Api_Structures": {
- fetchStructures: async () => ({}),
- dropStaleStructureTypes: async () => false,
- },
-});
-const pick = load("design_user_pick.ts", { "./design_page_data": data });
-
-const P = "p1";
-let reveals = 0;
-/** 폼이 고른 것을 실을 때마다 부르는 펴기(`design_shared` onReveal 그대로). */
-const onReveal = () => pick.isUserPick(P) && (reveals += 1);
-/** 브라우저가 그 이벤트를 처리하는 동안(그 마이크로태스크까지) — 끝나면 이벤트 없음. */
-async function during(event, body) {
- globalThis.event = event;
- try {
- await body();
- } finally {
- globalThis.event = undefined;
- }
-}
-const trusted = (type) => ({ type, isTrusted: true });
-const count = async (event, body) => {
- const before = reveals;
- await during(event, body);
- return reveals - before;
-};
-
-(async () => {
- const out = {};
- // 사용자 고름 — 종단 알약 · 횡단 카드(click) · 3D 부재(pointerup) · 우클릭 추가(contextmenu)
- out.click = await count(trusted("click"), () => onReveal());
- out.pointerup = await count(trusted("pointerup"), () => onReveal());
- out.contextmenu = await count(trusted("contextmenu"), () => onReveal());
- // 누름 밖 — 배수유역 다시 나눔 응답 · 횡단 다시 그림(서버 응답 · 타이머)
- out.outside = await count(undefined, () => onReveal());
- // 방향키로 교점 옮김 · 끌기 도중(pointermove)
- out.arrow = await count(trusted("keydown"), () => onReveal());
- out.drag = await count(trusted("pointermove"), () => onReveal());
- // 스크립트가 낸 가짜 누름
- out.synthetic = await count({ type: "click", isTrusted: false }, () => onReveal());
- // 교점 끌기 놓음 · 곡선 지우기 · 되돌리기 단추 — 묶음 차례가 떠 그 마이크로태스크에서 관 · 구조물 따라가며 다시 칠함
- let finish;
- out.pipelineRepaint = await count(trusted("pointerup"), async () => {
- data.trackPageBusy(P, new Promise((resolve) => (finish = resolve)));
- await Promise.resolve();
- onReveal(); // follow → 폼 다시 실음
- });
- out.busyPick = await count(trusted("click"), () => onReveal()); // 차례가 도는 사이 새 고름도 폄(505-2)
- finish();
- await new Promise((resolve) => setTimeout(resolve, 0));
- out.afterRun = await count(trusted("click"), () => onReveal()); // 차례가 끝나면 다시 폄
- process.stdout.write(JSON.stringify(out));
-})().catch((error) => {
- console.error(error);
- process.exit(1);
-});
diff --git a/resources/tester/test_132_b08_hydro_verdict_color.py b/resources/tester/test_132_b08_hydro_verdict_color.py
deleted file mode 100644
index 5ee50e3ce..000000000
--- a/resources/tester/test_132_b08_hydro_verdict_color.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""B08 수리 판정 글자색 (PLAN 132-1).
-
-O K = 초록 글(success) · N G = 붉은 글(danger) · 판정 없음 = 기본 글 · 배경 칠 없음.
-"""
-
-from __future__ import annotations
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-CSS = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro.css").read_text(encoding="utf-8")
-TS = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro_Sheet.ts").read_text(encoding="utf-8")
-
-
-def rule(selector: str) -> str:
- match = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", CSS)
- assert match, selector
- return match.group(1)
-
-
-def test_OK_는_success_글() -> None:
- assert "color: var(--color-success)" in rule(".b08-hydro__sheet.is-ok .is-key")
-
-
-def test_NG_는_danger_글() -> None:
- assert "color: var(--color-danger)" in rule(".b08-hydro__sheet.is-ng .is-key")
-
-
-def test_판정_없음은_기본_글() -> None:
- body = rule(".is-key.b08-hydro__out,\n.is-key.b08-hydro__verdict")
- assert "color: var(--color-text-body)" in body
- assert "--color-danger" not in body
-
-
-def test_배경_칠_없음() -> None:
- assert ".b08-hydro__verdict.is-ng" not in CSS
- assert "background" not in rule(".b08-hydro__sheet.is-ng .is-key")
-
-
-def test_판정이_장에_칠_갈래를_붙임() -> None:
- assert 'sheet.classList.toggle("is-ok", judged && r.ok === true)' in TS
- assert 'sheet.classList.toggle("is-ng", judged && !r.ok)' in TS
diff --git a/resources/tester/test_143_3_b08_material_cards.py b/resources/tester/test_143_3_b08_material_cards.py
deleted file mode 100644
index 926c7e346..000000000
--- a/resources/tester/test_143_3_b08_material_cards.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# -*- coding: utf-8 -*-
-"""B08 구조물 탭 — 자재 셋 값 없는 줄 숨김 · 표 넷 공용 카드 안(PLAN 143-3).
-
-숨김은 화면만 — 셈 · 열 · 단위수량은 그대로 · 고친 칸은 원래 줄 번호(`keep`)로 저장 자리를 찾음.
-"""
-
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + (
- ROOT / "B08_Quantity/B08_Quantity_Material_Tables.ts"
-).read_text(encoding="utf-8")
-SUMMARY = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") + (
- ROOT / "B08_Quantity/B08_Quantity_Summary_Table.ts"
-).read_text(encoding="utf-8")
-
-
-def test_자재_셋_값_없는_줄_숨김():
- assert (
- MATERIAL.count("const keep = shown(rows, ") == 2
- ) # 총괄 · 자재수량(콘크리트 = 늘 · 163-5)
- assert "row.qty != null || anyValue(row.units)" in MATERIAL # 손 단위수량 있으면 보임
- assert "row.qty != null || row.amount != null" in MATERIAL
- # 고친 칸 → 원래 줄(숨긴 줄 건너뜀)
- for target in (
- "rows[keep[i / 2]].id",
- "rows[keep[i]].key, key.slice(4)",
- "rows[keep[i]].panel",
- ):
- assert target in MATERIAL, target
-
-
-def test_표_넷_공용_카드_안():
- assert (
- "createCard({ body: [toolbar, table.root, excel, notes] })" in MATERIAL
- ) # + 엑셀 알림(160-4) · 안내 = 아래(202-1)
- assert "createCard({ body: [table, ...parts] })" in SUMMARY
diff --git a/resources/tester/test_160_2_b08_unassigned.py b/resources/tester/test_160_2_b08_unassigned.py
deleted file mode 100644
index c9fe2b6b1..000000000
--- a/resources/tester/test_160_2_b08_unassigned.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""160-2 B08 구조물 집계표 빈 조합 칸 글(198-1 부터 미연결 램프 호버 글 · 「미지정」 열 · 칸 표기는 지움).
-
-`B08_Quantity_Summary_Data.ts` 를 `common_util_summary_item.ts` 와 이어 붙여 node 로 돌림(61-1 시험과 같은 꼴).
-"""
-
-import json
-from pathlib import Path
-
-from test_61_1_b08_summary_table import _item, _run
-
-ROOT = Path(__file__).resolve().parents[2]
-
-
-def _slot(copy: str, group, item=None, linked=False) -> dict:
- return {"copy": copy, "group": group, "item": item, "linked": linked}
-
-
-FACILITIES = [
- {
- "id": "pipe@100",
- "type_id": "pipe",
- "name": "배관",
- "facility": "pipe",
- "chainage_m": 100.0,
- "slots": [
- _slot("pipe@100", None),
- _slot("pipe@100__gpipe", "관", _item("SS2")),
- _slot("pipe@100__gin", "관 보호공(유입)", _item("SS3"), linked=True),
- ],
- },
- {
- "id": "s1",
- "type_id": "ford_bridge",
- "name": "세월교",
- "facility": None,
- "chainage_m": 100.004,
- "slots": [_slot("s1__gfordout", "유출 구조", {"key": "깨짐"})], # 모양 틀림 = 조합 없음
- },
- {
- "id": "s2",
- "type_id": "box",
- "name": "BOX",
- "facility": None,
- "chainage_m": 160.0,
- "slots": [_slot("s2", None, _item("SS1"), linked=True)], # 빈 칸 없음 = 이름만
- },
-]
-
-
-def test_빈_조합_칸_글():
- out = _run(
- f"\nconst f = {json.dumps(FACILITIES, ensure_ascii=False)};"
- "\nconst at = (s) => unlinkedAt(f, s);"
- "\nconsole.log(JSON.stringify({"
- " t100: unassignedText(at(100), '본체'), t160: unassignedText(at(160), '본체'),"
- " t0: unassignedText(at(0), '본체') }));"
- )
- assert out["t100"] == "배관: 본체 · 관 / 세월교: 유출 구조"
- assert out["t160"] == "BOX" and out["t0"] == ""
-
-
-def test_화면_연결():
- summary = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8") + (
- ROOT / "B08_Quantity/B08_Quantity_Summary_Table.ts"
- ).read_text(encoding="utf-8")
- assert (
- 'unassignedText(unlinkedAt(unlinked, station), L("B08_Quantity_Link_Body"))' in summary
- ) # 칸 이름 = 모달과 같은 규칙
- assert 'key: "unassigned"' not in summary and "unassignedCombos" not in summary # 198-1
- locale = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
- assert "B08_Quantity_Summary_Unassigned" not in locale
diff --git a/resources/tester/test_161_1_b08_labor_card.py b/resources/tester/test_161_1_b08_labor_card.py
deleted file mode 100644
index d1708e07c..000000000
--- a/resources/tester/test_161_1_b08_labor_card.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""161-1 B08 구조물도 모달 — 일위대가 카드(보기 전용) · 사본 일위대가 읽기 · 카드 차례 도면 · 일위대가 · 면적산출."""
-
-import re
-from pathlib import Path
-
-DRAWING = Path("B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
-CARDS = Path("M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8")
-
-
-def test_labor_handle_passed():
- # createItemCards 에 labor 손잡이 — 없으면 카드 자체를 안 만듦(ItemCards `opts.labor ?`)
- call = DRAWING[DRAWING.index("createItemCards({") :]
- call = call[: call.index("});")]
- assert 'labor: () => openEdit(copy, name, "inputs", load)' in call # 163-3 넣을 값만
- assert "editable: () => true" in call
- assert "const laborCard = opts.labor" in CARDS
-
-
-def test_source_reads_copy_labor():
- # 142-1 고르기 모달과 같은 길 — 사본 일위대가 · 입력값 · 고른자재 → withCopyValues · 셈도 사본
- src = DRAWING[DRAWING.index("function sourceOf") : DRAWING.index("function comboSection")]
- assert "labor: {" in src
- for part in ('"일위대가"', '"입력값"', '"고른자재"'):
- assert f"items.read(folder, {part})" in src, part
- assert "withCopyValues(labor.줄, values, picks)" in src
- assert "items.calcLabor(folder)" in src
- assert re.search(r"if \(!labor\?\.줄\) throw", src) # 없으면 카드 「없음」
- assert "src.labor ? src.labor.read(at)" in CARDS
-
-
-def test_card_order_and_heads():
- assert "const cards: Card[] = [info, ...(laborCard ? [laborCard] : []), ...sheets]" in CARDS
- assert 'drawingInfo: "도면 정보"' in CARDS
- assert 'labor: "일위대가"' in CARDS and 'area: "면적산출"' in CARDS
diff --git a/resources/tester/test_163_b08_modal.py b/resources/tester/test_163_b08_modal.py
deleted file mode 100644
index bc44600bd..000000000
--- a/resources/tester/test_163_b08_modal.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# -*- coding: utf-8 -*-
-"""163 B08 구조물도 모달 — 연결 칩 · 접기(163-1) · 사본 고침(163-3) · [업데이트](163-4) · 콘크리트소요 줄(163-5).
-
-화면 글 판정 — 서버 · 짝 계산(Material_Calc)은 안 고침 · 고르기 모달 편집 화면은 재사용만.
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8")
-DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
-CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8")
-MATERIAL = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + (
- ROOT / "B08_Quantity/B08_Quantity_Material_Tables.ts"
-).read_text(encoding="utf-8")
-LOCALE = (ROOT / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
-
-
-def test_163_1_칩_한_줄_공용_접기():
- assert 'line.className = "b08-link__row"' in LINK # 시설마다 한 줄
- assert '"b08-link__chip is-linked"' in LINK and "chip.dataset.b08LinkSlot = slot.copy" in LINK
- # 모두 연결이면 접힘 · 미연결 있으면 펼침
- assert 'ui-collapsible${missing ? "" : " is-collapsed"}' in LINK
- assert "ui-collapsible__title" in LINK and "attachCollapsible(section)" in LINK
- assert "state.title = state.textContent" in LINK # 긴 이름 = 툴팁
- assert re.search(r"\.b08-link__row \{[^}]*flex-wrap: wrap", CSS)
- assert ".b08-link__slot" not in CSS + LINK # 옛 줄 꼴 없음
-
-
-def test_163_3_수정_사본_편집_화면():
- assert "editable: () => true" in DRAWING
- assert "onEdit: (kind) => openEdit(copy, name, kind, load)" in DRAWING
- assert 'labor: () => openEdit(copy, name, "inputs", load)' in DRAWING
- assert 'import("@ui/ui_template_item_picker_edit")' in DRAWING
- assert "createEditView(" in DRAWING and "items: projectItems(request.projectId)" in DRAWING
- assert "info: { 폴더: copy.폴더, 원본: copy.항목 }" in DRAWING
- # [뒤로] = 몸통 되돌림 · 저장했으면 카드 다시 읽음 · 닫을 때 집계표 다시 셈
- assert "body.replaceChildren(...kept)" in DRAWING and "if (saved) reload()" in DRAWING
- assert "if (edited) request.onLinked()" in DRAWING
- assert "if (editView && !(await editView.leave())) return false" in DRAWING
- assert 'kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))]' in DRAWING
- assert "saveTemplate" not in DRAWING # 층 쓰기 없음 — 사본 PUT 만(편집 화면 `items.write`)
-
-
-def test_163_4_배지_업데이트():
- assert ".list(folder)" in LINK and "structureFolder(slot.copy)" in LINK
- for key in ("Edited", "Gone", "Changed"):
- assert f'badge("B08_Quantity_Link_{key}"' in LINK, key
- assert "button.disabled = !info.마스터바뀜 || !!info.원본없음" in LINK
- assert "updateButton.disabled = true" in LINK # 대조 받기 전 꺼짐
- assert "projectItems(projectId).reapply(info)" in LINK
- assert 'showConfirmDialog(ask, L("B08_Quantity_Link_Update"))' in LINK
- assert 'info.고침 ? ` ${L("B08_Quantity_Link_UpdateEdited")}`' in LINK
- # 저장 뒤 = [선택]과 같은 흐름(onLinked)
- assert "saved\n ? onLinked()" in LINK
-
-
-def test_163_5_콘크리트_5줄_늘_까닭():
- table = MATERIAL[
- MATERIAL.index("function concreteTable") : MATERIAL.index("function totalTable")
- ]
- assert "const keep = rows.map((_row, i) => i)" in table
- assert 'editable: (_i, key) => key.startsWith("per:")' in table # 배합 늘 고침
- assert "function concreteWhy(" in MATERIAL
- assert "matched(m.name, m.spec, m.unit)" in MATERIAL # 짝 계산 맞춤 그대로
- assert "source.snapshot?.units?.[row.id]" in MATERIAL
- assert "concreteNote.textContent = concreteWhy(sheets, payload.source)" in MATERIAL
- assert "Material_Calc" in MATERIAL and "concreteWhy" not in (
- ROOT / "B08_Quantity/B08_Quantity_Material_Calc.ts"
- ).read_text(encoding="utf-8")
-
-
-def test_locale_keys_exist():
- for text in (LINK, DRAWING):
- for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', text)) | set(
- re.findall(r'badge\("([A-Za-z0-9_]+)"', text)
- ):
- assert f"{key}:" in LOCALE or key.startswith(("B08_Quantity_Summary_", "Common_")), key
diff --git a/resources/tester/test_170_b08_modal_head.py b/resources/tester/test_170_b08_modal_head.py
deleted file mode 100644
index ddde235dc..000000000
--- a/resources/tester/test_170_b08_modal_head.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# -*- coding: utf-8 -*-
-"""170 B08 구조물도 모달 — 머리 단추(170-2) · 칩 테두리 상태 색(170-3) · 집계표 보조 탭 줄 자리(170-4).
-
-화면 글 판정 — 공용 모달(ui_template_modal)은 안 고침.
-"""
-
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-PAGE = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
-PAGE_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
-
-
-def test_170_4_집계표_보조_탭_줄_자리():
- branch = PAGE.split('if (tab === "summary" && projectId) {', 1)[1].split("\n }", 1)[0]
- # 자재 탭 보조 탭 줄과 같은 클래스 · sm 단추 하나(보이지 않음 · 초점 안 감)
- assert '"b08-quantity__tabs ui-subtabs b08-quantity__tabs--blank"' in branch # 279 공용 양식
- assert 'size: "sm"' in branch and "ghost.tabIndex = -1" in branch
- assert 'setAttribute("aria-hidden", "true")' in branch
- # 집계표는 줄 아래 몸통에 그림(replaceChildren 이 빈 줄을 지우지 않게)
- assert (
- "view.append(blank, body)" in branch and "renderStructureSummary(body, projectId)" in branch
- )
- assert ".b08-quantity__tabs--blank > .ui-btn {\n visibility: hidden;" in PAGE_CSS.replace(
- "\r\n", "\n"
- )
-
-
-LINK = (ROOT / "B08_Quantity/B08_Quantity_UI_Link.ts").read_text(encoding="utf-8")
-DRAWING_CSS = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8")
-THEME = (ROOT / "ui_template/ui_template_theme.css").read_text(encoding="utf-8")
-
-
-def test_170_3_칩_테두리_상태_색():
- assert 'slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing"' in LINK
- assert "paintCopyState(projectId, slot, chip, badges, updateButton)" in LINK
- # 앞섬 = 없어짐 › 바뀜 › 고침 › 연결
- tone = LINK.split("const tone = info.원본없음", 1)[1].split(";", 1)[0]
- assert tone.index('"is-gone"') < tone.index('"is-changed"') < tone.index('"is-edited"')
- assert 'chip.classList.replace("is-linked", tone)' in LINK
- for cls, token in (
- ("is-linked", "success"),
- ("is-missing", "danger"),
- ("is-changed", "warning"),
- ("is-edited", "info"),
- ("is-gone", "danger"),
- ):
- rule = DRAWING_CSS.split(f".b08-link__chip.{cls} {{", 1)[1].split("}", 1)[0]
- assert f"border-color: var(--color-{token});" in rule
- assert f"--color-{token}:" in THEME # 밝음 · 어두움 토큰
- assert (
- "border-style: dashed;"
- in DRAWING_CSS.split(".b08-link__chip.is-gone {", 1)[1].split("}", 1)[0]
- )
-
-
-DRAWING = (ROOT / "B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
-EDIT = (ROOT / "ui_template/ui_template_item_picker_edit.ts").read_text(encoding="utf-8")
-PICKER = (ROOT / "ui_template/ui_template_item_picker.ts").read_text(encoding="utf-8")
-MODAL = (ROOT / "ui_template/ui_template_modal.ts").read_text(encoding="utf-8")
-
-
-def test_170_2_편집_화면_단추_모달_머리():
- # 편집 화면이 두 단추를 내보냄 · 고르기 모달(142-1)은 그대로 안 씀
- assert "back: HTMLButtonElement;" in EDIT and "save: HTMLButtonElement;" in EDIT
- assert "return { root, back, save," in EDIT
- assert "view.back" not in PICKER and "view.save" not in PICKER
- # 빈 머리 칸을 열 때 미리 넘김(공용 모달은 mount 때 자식 있을 때만 머리에 붙임)
- assert "if (actions.childElementCount) closeButton.before(actions)" in MODAL
- assert "actions: [head]," in DRAWING and "head.hidden = true;" in DRAWING
- # 들어갈 때 옮김 · 크기 = [닫기](md) · 차례 [← 뒤로] [저장] [닫기]
- assert 'view.back.classList.remove("ui-btn--sm")' in DRAWING
- assert 'view.save.classList.remove("ui-btn--sm")' in DRAWING
- assert "head.replaceChildren(view.back, view.save);\n head.hidden = false;" in DRAWING
- # 나올 때([뒤로] · 닫기) 거둠
- back = DRAWING.split("(saved) => {", 1)[1].split("},", 1)[0]
- assert "clearHead();" in back
- closing = DRAWING.split("beforeClose: async () => {", 1)[1].split("return true;", 1)[0]
- assert "clearHead();" in closing
- css = DRAWING_CSS
- assert ".b08-sheets .ui-modal__actions[hidden] {\n display: none;" in css
diff --git a/resources/tester/test_171_2_group_cards.py b/resources/tester/test_171_2_group_cards.py
deleted file mode 100644
index a57b32cff..000000000
--- a/resources/tester/test_171_2_group_cards.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""171-2 묶음 카드 공용 양식(`ui_template_group_cards`) — Node(타입 지움) + 작은 가짜 DOM.
-
-- 머리 = 호표 · 명칭 · 규격 · 단위 · 합계 / 몸 = 이름 · 규격 · 수량 · 단위 · 합계 · 비고 / 합계 줄 = 노무 · 재료 · 경비 · 계
-- 줄 금액 = 합계만 · 비목 셋 = 풍선 · 숫자 오른쪽 · 글자 14px 한 벌 · 줄 간격 좁게(공용 카드 16px 끼움 없음)
-- 가로 열 수 1~4(기본 2 · 고르면 알림) · 접기 없음(181-2) · 이동해 온 박스 = `is-pick`(붉은 외곽선) · 머리 누름 = 이동
-- 157-2 산근 카드 파일 = 이 양식으로 갈음(지움)
-"""
-
-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, run # noqa: E402
-
-GROUPS_JS = """
-const mk = (n) => ({
- key: `G${n}`, at: n * 10, no: `제 ${n} 호표`, name: `묶음${n}`, spec: "규격", specTip: "원문번호 1",
- unit: "m3", sum: 1234.5,
- lines: [
- { at: n * 10 + 1, name: "석공", spec: "", qty: 0.072, unit: "인", sum: 18643.3,
- parts: [18643.3, null, null], note: "노무3", link: true },
- { at: n * 10 + 2, name: "인용", tag: "품셈" },
- ],
- foot: { at: n * 10 + 3, parts: [100, 20, 3], sum: 123, note: "빈 줄 1", tip: "빈 줄 — 모래" },
-});
-const picks = [];
-const cols = [];
-const make = (count, extra = {}) => createGroupCards({
- groups: Array.from({ length: count }, (_, i) => mk(i + 1)),
- onPick: (at) => picks.push(at), onColumns: (n) => cols.push(n), ...extra,
-});
-"""
-
-
-def _run(body):
- return run([COMMON], GROUPS_JS + body)
-
-
-def test_도우미_열수_요약_풍선():
- out = _run(
- "\nconsole.log(JSON.stringify({"
- " cols: [0, 1, 2, 3, 4, 5, '3', null, undefined].map(clampColumns),"
- " tip: [partsTip([1000, 0, null]), partsTip([null, null, null]), partsTip(null)] }));"
- )
- assert out["cols"] == [2, 1, 2, 3, 4, 2, 2, 2, 2]
- assert out["tip"] == ["노무 1,000 · 재료 0 · 경비 -", None, None]
-
-
-def test_카드_머리_몸_합계_줄():
- out = _run(
- "\nconst root = make(1);"
- "\nconst box = root.querySelector('.ui-group-card');"
- "\nconst head = box.querySelector('.ui-group-card__head');"
- "\nconst tds = box.querySelector('tbody').children[0].children;"
- "\nconst ths = box.querySelector('thead').children[0].children;"
- "\nconsole.log(JSON.stringify({"
- " head: head.children.map((c) => [c.className, c.textContent, c.title]),"
- " ths: ths.map((c) => [c.textContent, c.className]),"
- " tds: tds.map((c) => [c.textContent, c.className, c.title]),"
- " tag: box.querySelector('.ui-group-card__tag').textContent,"
- " foot: box.querySelector('.ui-group-card__foot').children.map((c) => c.textContent),"
- " link: box.querySelector('tbody').children.map((r) => r.classList.contains('is-link')),"
- " cols: root.querySelector('.ui-group-cards__grid').style.props['--group-cols'],"
- " summary: root.all('.ui-btn').map((b) => b.textContent) }));"
- )
- assert out["head"] == [
- ["ui-group-card__no", "제 1 호표", ""],
- ["ui-group-card__title", "묶음1", ""],
- ["ui-group-card__spec", "규격", "원문번호 1"],
- ["ui-group-card__unit", "m3", ""],
- ["ui-group-card__total", "1,234.5", ""],
- ]
- assert out["ths"] == [
- ["이름", ""], ["규격", ""], ["수량", "is-num"], ["단위", ""], ["합계", "is-num"], ["비고", ""],
- ] # fmt: skip
- # 줄 금액 = 합계만 · 비목 셋 = 풍선 · 숫자 칸 = 오른쪽(is-num)
- assert out["tds"] == [
- ["석공", "ui-group-card__name", ""],
- ["", "", ""],
- ["0.1", "is-num", ""],
- ["인", "", ""],
- ["18,643.3", "is-num", "노무 18,643.3 · 재료 - · 경비 -"],
- ["노무3", "ui-group-card__note", ""],
- ]
- assert out["tag"] == "품셈"
- assert out["foot"] == ["노무 100", "재료 20", "경비 3", "계 123", "빈 줄 1"]
- assert out["link"] == [True, False]
- assert out["cols"] == "2" # 기본 2
- assert out["summary"] == ["1", "2", "3", "4"] # 가로 열 수 단추뿐(요약 · 접기 단추 없음)
-
-
-def test_가로_열수_고름_알림_누름_이동():
- out = _run(
- "\nconst root = make(2, { columns: 3 });"
- "\nconst grid = root.querySelector('.ui-group-cards__grid');"
- "\nconst before = grid.style.props['--group-cols'];"
- "\nroot.querySelector('[data-group-cols=\"4\"]').click();"
- "\nroot.querySelector('[data-at=\"21\"]').click();"
- "\nroot.querySelector('[data-at=\"20\"]').click();"
- "\nconsole.log(JSON.stringify({ before, after: grid.style.props['--group-cols'], cols,"
- " pressed: root.all('[data-group-cols]').map((b) => b.attrs['aria-pressed']), picks }));"
- )
- assert out["before"] == "3" and out["after"] == "4"
- assert out["cols"] == [4]
- assert out["pressed"] == ["false", "false", "false", "true"]
- assert out["picks"] == [21, 20] # 줄 · 머리 누름 = 표 줄 번호 그대로(갈 곳은 부르는 쪽 link)
-
-
-def test_묶음_많아도_접힘_없음_고른_박스_붉은_외곽선():
- out = _run(
- "\nconst root = make(13, { focus: 52 });"
- "\nconst cards = root.all('.ui-group-card');"
- "\nconst focused = root.querySelector('[data-at=\"52\"]');"
- "\nconst first = cards.map((c) => c.classList.contains('is-pick'));"
- "\ncards[0].querySelector('.ui-group-card__head').click();"
- "\nconsole.log(JSON.stringify({ first, picks,"
- " closed: cards.map((c) => c.classList.contains('is-closed')),"
- " buttons: root.all('.ui-btn').map((b) => b.textContent),"
- " foot: cards.every((c) => c.querySelector('.ui-group-card__foot')),"
- " focus: [focused.classList.contains('is-focus'), cards[4].scrolled === true] }));"
- )
- # 이동해 온 줄(5번째 묶음 줄) = 그 박스만 붉은 외곽선 · 박스 스크롤(248-1 줄 강조 안 붙임)
- assert out["first"] == [False] * 4 + [True] + [False] * 8
- assert out["focus"] == [False, True]
- assert out["closed"] == [False] * 13 # 13 묶음도 접힘 없음
- assert out["picks"] == [10] # 머리 누름 = 이동(표 줄 번호 그대로)
- assert out["buttons"] == ["1", "2", "3", "4"] # 모두 펼침 · 접기 단추 없음
- assert out["foot"] is True
-
-
-def test_고른_박스_없으면_외곽선_없음():
- out = _run(
- "\nconst root = make(3);"
- "\nconsole.log(JSON.stringify(root.all('.ui-group-card').map("
- "(c) => c.classList.contains('is-pick'))));"
- )
- assert out == [False, False, False]
-
-
-def test_양식_글자_간격_숫자():
- css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8")
- theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
- assert "--text-body-sm: 14px;" in theme
- assert "font-size: var(--text-body-sm);" in css # 14px 한 벌
- assert [ln.strip() for ln in css.splitlines() if "font-size" in ln] == [
- "font-size: var(--text-body-sm);",
- "font-size: inherit;",
- ]
- assert "padding: 1px var(--spacing-4);" in css # 줄 간격 좁게
- assert "is-closed" not in css and "is-closed" not in (ROOT / COMMON).read_text("utf-8")
- assert "border-color: var(--color-danger);" in css # 고른 박스 = B06 고름 꼴(181-2)
- assert "spacing-16" not in css and "card-padding" not in css # 공용 카드 16px 끼움 없음
- assert "grid-template-columns: repeat(var(--group-cols, 2), minmax(0, 1fr));" in css
- assert "text-align: right;" in css
- src = (ROOT / COMMON).read_text("utf-8")
- assert "createCard" not in src
- assert not (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun_Cards.ts").exists()
diff --git a/resources/tester/test_171_3_b09_group_tabs.py b/resources/tester/test_171_3_b09_group_tabs.py
deleted file mode 100644
index 36668ade1..000000000
--- a/resources/tester/test_171_3_b09_group_tabs.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""171-3 B09 묶음 탭 셋 = 묶음 카드 기본 · [표로 보기] = 표 그대로 · 보기 기억(탭마다).
-
-- 일위대가표 · 중기사용료 · 단가산출근거 — 표 줄을 호표마다 묶음(값 · 풍선 · 누름 이동 = 표와 같음)
-- 기억 = `b_page_state` 두 줄(`b09-group-view` · `b09-group-cols`) · 탭마다 따로 · 모르는 값 = 카드 · 2
-- 집계표 넷 · 설계내역서 · B08 = 대상 밖
-"""
-
-import json
-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": "찰쌓기", "spec": "기계=굴착기 0.6㎥", "spec_raw": "기계=0201-0070",
- "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,
- "blank": 1, "blank_names": ["모래"],
-} # fmt: skip
-
-MB = {
- "key": "EQ1", "name": "굴착기", "spec": "0.6㎥", "unit": "hr",
- "lines": [
- {"kind": "expense", "name": "손료", "spec": "", "qty": 1, "unit": "hr", "price": 13041,
- "amount": 13041, "ref": None},
- {"kind": "labor", "name": "운전사", "spec": "", "qty": 1, "unit": "인", "price": 55700,
- "amount": 55700, "ref": {"key": "LB9"}},
- ],
- "sum": 68741, "labor": 55700, "material": 0, "expense": 13041,
-} # fmt: skip
-
-SG = {
- "key": "SG:A", "no": 1, "name": "뒷채움", "unit": "m3", "spec": "규격=150", "spec_raw": "규격=150",
- "head": ["뒷채움 / m3", "산림품셈 13-3-2 적용"],
- "lines": [
- {"kind": "labor", "name": "보통인부", "ref": {"tab": "labor", "key": "LB2"}, "why": None,
- "labor": 2979.8, "material": 0.0, "expense": 0.0, "text": ["보통인부 : 165,545 * 0.018 = 2,979.8"]},
- ],
- "labor": 2979, "material": 0, "expense": 0, "sum": 2979, "why": None, "foot": "계 : 2,979",
-} # fmt: skip
-
-# 카드 → 판정 거리(머리 · 몸 줄 · 합계 · 표 줄 번호 · 값이 표와 같은가 · 누름 이동)
-DUMP = """
-const dump = (tab, t) => {
- const cards = tabGroups(tab, t);
- const first = t.columns[0].key;
- return {
- n: t.rows.length,
- heads: cards.map((c) => [c.no, c.name, c.spec, c.specTip, c.unit, c.sum]),
- lines: cards.flatMap((c) => c.lines.map((l) => [l.at, l.tag ?? null, l.name, l.qty ?? null, l.sum, l.parts, l.note])),
- foot: cards.map((c) => [c.foot.at, c.foot.parts, c.foot.sum, c.foot.note, c.foot.tip]),
- ats: cards.flatMap((c) => [c.at, ...c.lines.map((l) => l.at), c.foot.at]),
- same: cards.every((c) => c.lines.every((l) => l.sum === (t.rows[l.at].sum ?? null))),
- links: cards.flatMap((c) => c.lines.map((l) => (l.link ? t.link(l.at, first) : null))),
- };
-};
-"""
-
-
-def _tab(files, build, sheets, tab):
- body = f"\nconsole.log(JSON.stringify(dump('{tab}', {build}({{ sheets: {json.dumps(sheets, ensure_ascii=False)} }}))));"
- prelude = "" if any("Sangeun" in f for f in files) else RAW # raw 짝 = 산근 파일에 있음
- return run([COMMON, TABS, *files, GROUPS], DUMP + body, prelude)
-
-
-def test_일위대가표_카드():
- out = _tab([f"{B09}Labor.ts"], "buildDaegaBlocks", {"daega_blocks": [DG]}, "dg")
- assert out["heads"] == [
- ["제 1 호표", "찰쌓기", "기계=굴착기 0.6㎥", "원문번호 기계=0201-0070", "m2", 22807]
- ]
- assert out["lines"] == [
- [1, None, "건설표준품셈 7-1-2", None, None, [None, None, None], "품셈 인용"],
- [2, None, "석공", 0.072, 18643.3, [18643.3, None, None], "노무3"],
- [3, None, "뒷채움", 0.5, 4164, [3439, 269, 456], "산근 1호표"],
- ]
- assert out["foot"] == [[4, [22082, 269, 456], 22807, "빈 줄 1", "빈 줄 — 모래"]]
- assert out["ats"] == list(range(out["n"])) and out["same"] is True
- assert out["links"] == [None, {"tab": "labor", "key": "L00005"}, {"tab": "sg", "key": "SG1"}]
-
-
-def test_중기사용료_카드():
- out = _tab([f"{B09}Base.ts"], "buildMachineBlocks", {"machine_blocks": [MB]}, "machine_blocks")
- assert out["heads"] == [["제 1 호표", "굴착기", "0.6㎥", None, "hr", 68741]]
- assert out["lines"] == [
- [1, None, "손료", 1, 13041, [None, None, 13041], ""],
- [2, None, "운전사", 1, 55700, [55700, None, None], ""],
- ]
- assert out["foot"] == [[3, [55700, 0, 13041], 68741, "", None]]
- assert out["ats"] == list(range(out["n"])) and out["same"] is True
- assert out["links"] == [None, {"tab": "basis", "key": "LB9"}]
-
-
-def test_단가산출근거_카드():
- out = _tab([f"{B09}Sangeun.ts"], "buildSg", {"sg": [SG]}, "sg")
- assert out["heads"] == [["제 1 호표", "뒷채움", "규격=150", None, "m3", 2979]]
- assert out["lines"] == [
- [1, "", "산림품셈 13-3-2 적용", None, None, [None, None, None], ""],
- [2, "노무", "보통인부 : 165,545 * 0.018 = 2,979.8", None, 2979.8, [2979.8, 0, 0], ""],
- ]
- assert out["foot"][0][:3] == [3, [2979, 0, 0], 2979]
- assert out["ats"] == list(range(out["n"])) and out["same"] is True
- assert out["links"] == [None, {"tab": "labor", "key": "LB2"}]
-
-
-def test_보기_한_값_열_수_탭마다():
- out = run(
- [COMMON, TABS, GROUPS],
- "\nconst a = [readGroupView(), readGroupCols('dg')];"
- "\nkept.set('b09-group-view', JSON.stringify({ machine_blocks: 'table' }));" # 옛 탭별 값
- "\nrememberGroup('b09-group-cols', 'sg', 3);"
- "\nconst b = [groupView(), readGroupView(), readGroupView(),"
- " readGroupCols('sg'), readGroupCols('dg'), readGroupCols('dg', { dg: 9 })];"
- "\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept),"
- " tabs: GROUP_TABS, other: ['bill', 'sum_material', 'sum_labor', 'sum_expense',"
- " 'sum_machine', 'dg_list'].map(isGroupTab) }));",
- )
- assert out["a"] == ["cards", 2] # 기본 = 카드 · 가로 2
- assert out["b"] == ["table", "table", "table", 3, 2, 2] # 304-1 셋 탭 한 값
- assert json.loads(out["kept"]["b09-group-view"]) == "table" # 첫 읽기에 옛 값 → 한 값
- assert json.loads(out["kept"]["b09-group-cols"]) == {"sg": 3}
- assert out["tabs"] == ["dg", "machine_blocks", "sg"]
- assert out["other"] == [False] * 6 # 집계표 넷 · 설계내역서 · 목록표 = 대상 밖
-
-
-def test_화면_보기_고름_누름_이동():
- out = run(
- [COMMON, TABS, f"{B09}Labor.ts", GROUPS],
- f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
- "\nconst gone = []; let redraws = 0; const focused = []; const calls = [];"
- "\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: (l) => gone.push(l),"
- " redraw: () => redraws++, setTools: (t) => calls.push(t), makeTable: () => ({ root: document.createElement('table'),"
- " focusRow: (i) => focused.push(i) }) };"
- "\nconst [view] = renderGroupPanel(o);"
- "\nconst label = calls.flat().filter((e) => e.dataset.b09GroupView).length;"
- "\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
- "\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');"
- "\nview.querySelector('[data-at=\"2\"]').click();"
- "\nview.querySelector('[data-at=\"0\"]').click();"
- "\nconst before = calls.length; chooseGroupView('table');"
- "\nconst [view2] = renderGroupPanel(o);"
- "\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
- " tools2: calls.length - before, tag2: view2.tagName, focused,"
- " kept: Object.fromEntries(kept) }));",
- RAW,
- )
- assert out["label"] == 0 and out["table"] == "dg" # 기본 = 카드 · 304-1 탭 줄 보기 단추 없음
- assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동
- assert out["lit"] is False # 248-1 이동해 온 줄 = 빨강 하나(줄 강조 안 붙임)
- assert out["box"] is True # 그 호표 박스 = 붉은 외곽선(181-2)
- assert out["redraws"] == 0
- assert out["tools2"] == 0 and out["tag2"] == "TABLE" and out["focused"] == [3]
- assert json.loads(out["kept"]["b09-group-view"]) == "table"
-
-
-def test_잇기():
- shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert (
- "if (isGroupTab(tab)) {" in shell
- and "renderGroupPanel({" in shell
- and "setTools: bars.setTools" in shell
- )
- state = (ROOT / "A00_Common" / "b_page_state.ts").read_text("utf-8")
- assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state
- assert '"b09-group-cols": { bucket: "pref", scope: "global" },' in state
- b08 = "".join(p.read_text("utf-8") for p in (ROOT / "B08_Quantity").glob("*.ts"))
- assert "createGroupCards" not in b08 # B08 = 대상 밖
diff --git a/resources/tester/test_181_1_b09_tab_groups.py b/resources/tester/test_181_1_b09_tab_groups.py
deleted file mode 100644
index 09b83bd94..000000000
--- a/resources/tester/test_181_1_b09_tab_groups.py
+++ /dev/null
@@ -1,102 +0,0 @@
-"""181-1 B09 탭 두 층 — 위 탭 8(203-1 연결 점검 뺌 · 257-1 설계내역 한 묶음) · 아래 탭 묶음(사용자 30번) · 줄 그리기 · 마지막 본 아래 탭.
-
-- 위 탭 = `TAB_GROUPS`(`TABS` 18 키를 빠짐 · 겹침 없이 묶음) · `TABS`(엑셀 차례)는 그대로
-- 줄 = 위 md · 아래 sm · 아래 탭 없는 위 탭 = 빈 보조 줄(B08 꼴)
-"""
-
-import json
-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 TABS, run # noqa: E402
-
-BAR = "B09_Estimation/B09_Estimation_UI_TabBar.ts"
-
-# 사용자 30번 — 위 탭 차례 = 지금 탭 차례(묶음 첫 자리) · 아래 = 글에 적힌 차례
-EXPECTED = [
- ["공사원가계산서", ["cost"]],
- ["설계내역", ["total", "bill"]],
- ["일위대가", ["dg_list", "dg"]],
- ["단가산출", ["sg_list", "sg"]],
- ["환율및기초자료", ["basis"]],
- ["중기", ["machine", "machine_blocks", "sum_machine", "expense", "sum_expense"]],
- ["재료", ["material", "compare", "sum_material"]],
- ["노무", ["labor", "sum_labor"]],
-]
-
-
-def test_묶음_표_사용자_30번과_같고_18_탭이_한_번씩():
- out = run(
- [TABS],
- "\nconsole.log(JSON.stringify({ groups: TAB_GROUPS, ids: TABS.map(([t]) => t),"
- " of: TABS.map(([t]) => groupOf(t)[0]) }));",
- )
- assert out["groups"] == EXPECTED
- flat = [t for _, tabs in out["groups"] for t in tabs]
- assert sorted(flat) == sorted(out["ids"]) and len(flat) == len(set(flat)) == 18
- # 위 탭 차례 = 엑셀 차례에서 묶음이 처음 나오는 자리
- firsts = list(dict.fromkeys(out["of"]))
- assert firsts == [g[0] for g in EXPECTED]
- assert len(firsts) == 8
-
-
-def test_줄_그리기_두_층_빈_줄_마지막_본_아래_탭():
- out = run(
- [TABS, BAR],
- """
-const picks = [];
-const bars = createTabBars((t) => picks.push(t));
-const view = () => ({
- top: bars.top.children.map((b) => [b.textContent, b.className, b.attrs['aria-selected'],
- b.dataset.b09Group, b.dataset.b09Tab ?? null]),
- sub: bars.sub.children.filter((b) => !b.classList.contains('b09-estimation__tools')).map((b) => [b.textContent, b.attrs['aria-selected'], b.dataset.b09Tab ?? null]),
- blank: bars.sub.classList.contains('b09-estimation__tabs--blank'),
- role: bars.sub.attrs.role,
-});
-bars.draw('dg');
-const a = view();
-bars.top.children[2].click(); // 켜진 위 탭 다시 누름 = 아무 일 없음
-bars.sub.children[0].click(); // 일위대가목록표
-bars.draw('compare'); // 묶음을 넘는 이동 = 위 탭 따라 켜짐
-const b = view();
-bars.draw('sum_material');
-bars.draw('dg');
-bars.top.children[6].click(); // 재료 = 마지막 본 아래 탭(재료비수량금액집계표)
-bars.top.children[5].click(); // 중기 = 본 적 없음 → 첫 아래 탭
-bars.draw('cost');
-const c = view();
-bars.top.children[1].click(); // 설계내역 = 첫 아래 탭(총괄)
-bars.sub.children[0].click(); // 빈 줄 단추(숨김) = 누름 없음
-console.log(JSON.stringify({ a, b, c, picks }));
-""",
- )
- a, b, c = out["a"], out["b"], out["c"]
- assert len(a["top"]) == 8 and [t[0] for t in a["top"]][:5] == [
- "공사원가계산서", "설계내역", "일위대가", "단가산출", "환율및기초자료"
- ] # fmt: skip
- assert [t[2] for t in a["top"]] == ["false"] * 2 + ["true"] + ["false"] * 5
- assert all("ui-btn--ghost" in t[1] for t in a["top"] if t[2] == "false")
- assert a["sub"] == [["일위대가목록표", "false", "dg_list"], ["일위대가표", "true", "dg"]]
- assert not a["blank"] and a["role"] == "tablist"
- assert b["sub"] == [
- ["재료비목록표", "false", "material"],
- ["자재단가대비표", "true", "compare"],
- ["재료비수량금액집계표", "false", "sum_material"],
- ] # fmt: skip
- assert b["top"][6][2] == "true" and b["top"][2][2] == "false"
- # 단독 위 탭 — 위 단추가 탭 키도 가짐 · 아래 줄 = 숨긴 단추 한 개
- assert c["blank"] and c["role"] == "presentation" and len(c["sub"]) == 1
- assert c["top"][0][3] == "cost" and c["top"][0][4] == "cost" and c["top"][3][4] is None
- assert out["picks"] == ["dg_list", "sum_material", "machine", "total"]
-
-
-def test_잇기_쉘이_두_줄을_쓰고_CSS_빈_줄_규칙():
- shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert "createTabBars((tab) => show(tab))" in shell and "bars.draw(tab);" in shell
- assert "body.append(bars.top, bars.sub, panel);" in shell
- css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- assert ".b09-estimation__tabs--blank > .ui-btn" in css and "visibility: hidden" in css
diff --git a/resources/tester/test_181_2_b09_box_pick.py b/resources/tester/test_181_2_b09_box_pick.py
deleted file mode 100644
index 211dd7009..000000000
--- a/resources/tester/test_181_2_b09_box_pick.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""181-2 박스보기 — 이름 · 접기 없음 · 목록표에서 고른 호표 = 박스 붉은 외곽선 · 표 보기 = 줄 구간 전체.
-
-- 단추 글 「박스보기」 · 「호표보기」(304-1 좌측 스위치) · 요약(접기) 보기 지움
-- 표 보기 = 고른 호표 줄 구간(키 `|` 앞머리 같은 줄) 모두 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end`
-- 붉은 색 = `--color-danger`(B06 횡단 카드 고름 꼴)
-"""
-
-import json
-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, TABS, run # noqa: E402
-
-ROWS = [ # 호표 A(3줄) · 호표 B(4줄) · 호표 C(1줄)
- {"key": "A"}, {"key": "A|1"}, {"key": "A|합계"},
- {"key": "B"}, {"key": "B|1"}, {"key": "B|2"}, {"key": "B|총계"},
- {"key": "C"},
-] # fmt: skip
-
-
-def test_구간_첫_끝_줄():
- out = run(
- [COMMON, TABS, GROUPS],
- f"\nconst rows = {json.dumps(ROWS)};"
- "\nconst show = (focus) => { const r = pickRange(rows, focus);"
- " return [r, rows.map((_, i) => pickClass(r, i))]; };"
- "\nconsole.log(JSON.stringify({ a: show('A|1'), b: show('B'), c: show('C'),"
- " none: show(undefined), unknown: show('Z|1') }));",
- )
- assert out["a"][0] == [0, 2]
- assert out["a"][1] == [
- "is-pick is-pick-top", "is-pick", "is-pick is-pick-end", None, None, None, None, None,
- ] # fmt: skip
- assert out["b"][0] == [3, 6]
- assert out["b"][1][3:7] == [
- "is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end",
- ] # fmt: skip
- assert out["b"][1][:3] == [None] * 3 # 다른 호표 없음
- assert out["c"][0] == [7, 7] and out["c"][1][7] == "is-pick is-pick-top is-pick-end"
- assert out["none"] == [None, [None] * 8] and out["unknown"] == [None, [None] * 8]
-
-
-def test_표_보기_줄_class_넘김():
- out = run(
- [COMMON, TABS, GROUPS],
- f"\nconst spec = {{ columns: [{{ key: 'key' }}], rows: {json.dumps(ROWS)} }};"
- "\nlet extra = null;"
- "\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {}, setTools: () => {},"
- " makeTable: (e) => { extra = e; return { root: document.createElement('table'),"
- " focusRow: () => {} }; } };"
- "\nchooseGroupView('table');"
- "\nrenderGroupPanel(o);"
- "\nconsole.log(JSON.stringify(spec.rows.map((_, i) => extra(i))));",
- )
- assert out == (
- [None] * 3 + ["is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end"] + [None]
- )
-
-
-def test_글_접기_없음_CSS_잇기():
- groups = (ROOT / GROUPS).read_text("utf-8")
- assert '"박스보기" : "호표보기"' in groups and "호표별 보기" not in groups # 304-1 글
- common = (ROOT / COMMON).read_text("utf-8")
- for gone in ("isSummary", "GROUP_SUMMARY_OVER", "is-closed", "모두 펼침", "모두 접기"):
- assert gone not in common
- assert 'box?.classList.add("is-pick")' in common
- page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- for sel in ("tr.is-pick td", "tr.is-pick.is-pick-top td", "tr.is-pick.is-pick-end td"):
- assert sel in page
- assert (
- page.count("var(--color-danger)") == 5
- ) # 위 · 아래 · 왼 · 오른 2px + 204-1 끌기 못 놓음 점선
- shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert "extra?: (index: number) => string | null" in shell
diff --git a/resources/tester/test_183_2_b09_compare_head.py b/resources/tester/test_183_2_b09_compare_head.py
deleted file mode 100644
index 97ff1246e..000000000
--- a/resources/tester/test_183_2_b09_compare_head.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""183-2 자재단가대비표 머리 — 견적 「면」 뺌 · 견적서 = 견적 아래 · 다른 출처 「출처」 열은 185-4 에서 호버로.
-
-소스 시험(열 정의 · 고칠 칸) — 셈 · 저장(`quote_page` 남음)은 안 바뀜(test_166_4).
-"""
-
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-SRC = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8")
-
-
-def _compare() -> str:
- start = SRC.index("export const buildCompare")
- return SRC[start : SRC.index("export const buildMaterial")]
-
-
-def test_견적은_단가_견적서_그_밖은_단가_하나():
- # 185-4 — 출처 열 없앰(출처 뜻 = 단가 칸 호버 · test_185_4)
- body = _compare()
- assert '[money(`p:${s}`, "단가", [s]), text("files", "견적서", 72, [s])]' in body
- assert ': [money(`p:${s}`, "단가", [s])],' in body
- assert "g:${s}" not in body and '"출처", 64' not in body
- assert '"면"' not in body # 면 머리 없음
- assert body.count('text("files", "견적서"') == 1 # 맨 끝 견적서 열 지움
-
-
-def test_견적_면_수기_칸_없앰():
- body = _compare()
- assert 'key === "g:견적"' not in body and "quote_page" not in body
- assert "견적 단가는 수기 입력" in body and "견적 단가 · 면" not in body
diff --git a/resources/tester/test_185_2_b09_view_bar.py b/resources/tester/test_185_2_b09_view_bar.py
deleted file mode 100644
index e18aa71c4..000000000
--- a/resources/tester/test_185_2_b09_view_bar.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""185-2 B09 묶음 탭 가로 열 수 단추 한 줄 · 보기 고름 = 좌측 하단(304-1 · 탭 줄에 없음).
-
-- 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만
-- 단추 · 라벨 = `white-space: nowrap` · 도구 묶음 = 줄바꿈 없음(`flex-wrap: nowrap`)
-"""
-
-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}
-HINT = "(() => { const h = document.createElement('p'); h.className = 'hint'; 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"\nchooseGroupView('{view}');"
- "\nconst tools = [];"
- f"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}},"
- " setTools: (t) => tools.push(...t),"
- " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
- "\nconst out = renderGroupPanel(o);"
- "\nconst btn = tools.find((e) => e.dataset.b09GroupView);"
- "\nconst bar = out[0].all('.ui-group-cards__bar')[0] ?? null;"
- "\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn ? btn.textContent : null, nTools: tools.length,"
- " cols: tools.flatMap((e) => e.all('[data-group-cols]')).map((b) => b.dataset.groupCols),"
- " barBtn: bar ? bar.all('[data-b09-group-view]').length : null,"
- " barCols: bar ? bar.all('[data-group-cols]').length : null }));",
- RAW,
- )
-
-
-def test_카드_보기_단추가_열수_단추와_같은_도구_묶음에():
- out = _panel("cards", "null")
- assert out["label"] is None # 304-1 보기 단추 = 좌측 하단(탭 줄에 없음)
- assert out["cols"] == ["1", "2", "3", "4"] # 같은 묶음에 열 수 단추 넷
- assert out["barBtn"] == 0 and out["barCols"] == 0 # 카드 판 머리 띠에는 없음
- assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐
-
-
-def test_카드_보기_안내_글은_카드_판_아래에():
- # 202-1 — 안내 글 = 카드 판 아래(상세 = test_202_1)
- out = _panel("cards", HINT)
- assert out["n"] == 2 and out["first"] == "ui-group-cards"
-
-
-def test_표_보기_단추는_도구_묶음에():
- out = _panel("table", "null")
- assert out["label"] is None and out["n"] == 1
- assert out["cols"] == [] and out["nTools"] == 0 # 표 보기 = 도구 없음
-
-
-def test_CSS_한_줄_nowrap():
- cards = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8")
- assert "flex-wrap: nowrap" in _block(cards, ".ui-group-cards__bar")
- item = _block(
- cards,
- ".ui-group-cards__bar .ui-btn,\n.ui-group-cards__label,\n.ui-group-cards__count",
- )
- assert "white-space: nowrap" in item and "flex: none" in item
- page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- assert "flex-wrap: nowrap" in _block(page, ".b09-estimation__tools")
- tools = _block(
- page, ".b09-estimation__tools .ui-btn,\n.b09-estimation__tools .ui-group-cards__label"
- )
- assert "white-space: nowrap" in tools
- assert "toggle" not in (ROOT / GROUPS).read_text("utf-8") # 304-1 탭 줄 토글 걷음
diff --git a/resources/tester/test_187_1_b05_head_button_size.py b/resources/tester/test_187_1_b05_head_button_size.py
deleted file mode 100644
index 53f0f67fa..000000000
--- a/resources/tester/test_187_1_b05_head_button_size.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""187-1 B05 종단 그래프 머리 줄 — [초기화]~▼ 단추 크기 = [새 창] 크기([배수유역도] 단추는 282-1 로 지움).
-
-- 기준값 = [새 창] 단추의 계산 크기(`.ui-btn` 바탕 + 줄 단추 토큰) — 높이 16 · 글자 12 · 좌우 여백 8
-- 왼쪽 단추(`b05-route-profile__tool`) = 같은 토큰 · 고정 높이 · 최소 너비 없음
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-
-
-def _read(rel: str) -> str:
- return (ROOT / rel).read_text(encoding="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 _decl(block: str, prop: str) -> str:
- m = re.search(r"(?:^|;|\s)" + re.escape(prop) + r"\s*:\s*([^;]+);", block)
- assert m, prop
- return m.group(1).strip()
-
-
-THEME = _read("ui_template/ui_template_theme.css")
-BASE = _read("ui_template/ui_template_elements_styles.ts")
-LEFT = _block(_read("B05_Profile/B05_Profile_UI_Style.css"), ".b05-route-profile__tool")
-POPOUT = _block(
- _read("A00_Common/design/design_page.css"),
- ".b05-route-profile__tools > .b05-route-profile__popout",
-)
-
-
-def _px(value: str) -> int:
- return int(re.fullmatch(r"(\d+)px", value).group(1))
-
-
-def _token(name: str) -> str:
- return _decl(THEME, name)
-
-
-def _size(block: str) -> dict:
- """계산 크기 — padding · font-size 를 토큰으로 풀고 line-height 1 · 테두리 1px 로 높이를 셈."""
- pad = _decl(block, "padding")
- font = _decl(block, "font-size")
- if pad == "var(--btn-bar-padding)":
- pad = _token("--btn-bar-padding")
- if font == "var(--btn-bar-font-size)":
- font = _token("--btn-bar-font-size")
- v = re.fullmatch(r"(\d+)px var\(--spacing-8\)", pad).group(1)
- spacing8 = _px(_token("--spacing-8"))
- caption = _px(_token("--text-caption")) if font == "var(--text-caption)" else _px(font)
- return {"padV": int(v), "padH": spacing8, "font": caption, "height": caption + 2 * int(v) + 2}
-
-
-def test_기준값_오른쪽_단추_16_12_8():
- for block in (POPOUT,):
- assert _size(block) == {"padV": 1, "padH": 8, "font": 12, "height": 16}
- # 높이 계산의 바탕 = .ui-btn 줄 높이 1 · 테두리 1px
- assert re.search(r"\.ui-btn \{[^}]*line-height: 1;[^}]*border: 1px solid", BASE)
-
-
-def test_왼쪽_단추_높이_글자_여백이_오른쪽_기준값과_같음():
- assert _size(LEFT) == _size(POPOUT)
- assert _decl(LEFT, "line-height") == "1"
- assert re.search(r"border:\s*1px solid", LEFT)
-
-
-def test_왼쪽_단추에_옛_고정_크기_없음():
- assert "height:" not in LEFT.replace("line-height", "")
- assert "min-width" not in LEFT
- assert "9px" not in LEFT
-
-
-def test_두_단추_모두_같은_토큰():
- for block in (LEFT, POPOUT):
- assert _decl(block, "padding") == "var(--btn-bar-padding)"
- assert _decl(block, "font-size") == "var(--btn-bar-font-size)"
- assert _decl(LEFT, "font-family") == "var(--font-body)" # .ui-btn 바탕과 같은 글꼴
-
-
-def test_토큰은_theme_한_곳():
- assert _token("--btn-bar-padding") == "1px var(--spacing-8)"
- assert _token("--btn-bar-font-size") == "var(--text-caption)"
- assert _px(_token("--spacing-8")) == 8 and _px(_token("--text-caption")) == 12
diff --git a/resources/tester/test_200_1_b09_box_tool_line.py b/resources/tester/test_200_1_b09_box_tool_line.py
deleted file mode 100644
index b0c95cb85..000000000
--- a/resources/tester/test_200_1_b09_box_tool_line.py
+++ /dev/null
@@ -1,187 +0,0 @@
-"""200-1 B09 박스보기 안내 글 줄임표 + 호버 · 카드 같은 가로 줄 높이 같게.
-
-- 안내 글 = 203 줄 수 변수만큼(넘치면 스크롤) · 가로로 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문 · 자리 = 표 · 카드 판 아래(202-1 로 옮김)
-- 카드 격자 = 행 늘림(`align-items: stretch`) · 카드 = 세로 flex · 몸통이 늘어 합계 줄이 아래
-- 카드 제목 줄 · 합계 줄 바탕색(200-3)
-"""
-
-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"\nchooseGroupView('{view}');"
- f"\nconst hint = {hint};"
- "\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {}, setTools: () => {},"
- " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
- "\nconst out = renderGroupPanel(o);"
- "\nconst last = out[out.length - 1];"
- "\nconst inBar = hint ? out.some((e) => e.all('.ui-group-cards__bar').some((b) => b.contains(hint))) : false;"
- "\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, hintLast: !!hint && last === hint, inBar,"
- " hintCls: hint ? hint.className : '', hover: hoverCalls.length,"
- " hoverIsHint: call ? call.root === hint : false, wide, fit, first: out[0].tagName }));",
- RAW,
- )
-
-
-def test_카드_보기_안내_글은_카드_판_아래():
- out = _panel("cards", HINT)
- assert out["n"] == 2 and out["hintLast"] and not out["inBar"] # 카드 판 + 안내 글(맨 아래)
- assert "b09-estimation__line-hint" in out["hintCls"]
-
-
-def test_표_보기_안내_글은_표_아래():
- out = _panel("table", HINT)
- assert out["n"] == 2 and out["first"] == "TABLE" and out["hintLast"] # 표 + 안내 글
-
-
-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["n"] == 1 and out["hintLast"] 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 ( # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤
- "max-height: calc(var(--b09-hint-lines) * 1lh)",
- "overflow-y: auto",
- "min-width: 0",
- ):
- assert need in hint, need
-
-
-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 "attachHoverInfo(hint," in groups and "placeTools: o.setTools" in groups
- cards = (ROOT / COMMON).read_text("utf-8")
- assert "opts.lead" not in cards and "if (opts.placeTools) {" in cards
-
-
-# ── 200-3 카드 제목 줄 · 합계 줄 바탕색(밝음 · 어두움) ─────────────────────────
-
-
-def _theme_vars(dark: bool) -> dict:
- """테마 CSS 의 `:root`(밝음) · 어두움 덮어쓰기 토큰 → 한 벌."""
- css = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
- light = dict(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ":root")))
- if dark:
- light.update(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ':root[data-theme="dark"]')))
- return light
-
-
-def _rgb(token: str, theme: dict) -> tuple:
- """토큰 → (r, g, b) · var() 별칭 따라감 · 16진수만."""
- val = theme[token].split("/*")[0].strip()
- while m := re.fullmatch(r"var\((--[\w-]+)\)", val):
- val = theme[m.group(1)].split("/*")[0].strip()
- assert re.fullmatch(r"#[0-9a-fA-F]{6}", val), (token, val)
- return tuple(int(val[i : i + 2], 16) for i in (1, 3, 5))
-
-
-def _mix(spec: str, theme: dict) -> tuple:
- """`color-mix(in srgb, var(A) N%, var(B))` → 섞은 (r, g, b)."""
- m = re.fullmatch(
- r"color-mix\(in srgb,\s*var\((--[\w-]+)\)\s*(\d+)%,\s*var\((--[\w-]+)\)\)", spec
- )
- assert m, spec
- a, n, b = _rgb(m.group(1), theme), int(m.group(2)) / 100, _rgb(m.group(3), theme)
- return tuple(a[i] * n + b[i] * (1 - n) for i in range(3))
-
-
-def _lum(c: tuple) -> float:
- ch = [
- (v / 255) / 12.92 if v / 255 <= 0.03928 else (((v / 255) + 0.055) / 1.055) ** 2.4 for v in c
- ]
- return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2]
-
-
-def _contrast(a: tuple, b: tuple) -> float:
- hi, lo = sorted((_lum(a), _lum(b)), reverse=True)
- return (hi + 0.05) / (lo + 0.05)
-
-
-def _tokens() -> tuple:
- css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8")
- root = _block(css, ".ui-group-cards")
- head = re.search(r"--group-head-bg:\s*([^;]+);", root)
- foot = re.search(r"--group-foot-bg:\s*([^;]+);", root)
- assert head and foot
- return css, head.group(1), foot.group(1)
-
-
-def test_카드_제목_줄과_합계_줄_바탕색_토큰():
- css, head, foot = _tokens()
- assert head != foot
- assert "background-color: var(--group-head-bg)" in _block(css, ".ui-group-card__head")
- assert "background-color: var(--group-foot-bg)" in _block(css, ".ui-group-card__foot")
- for spec in (head, foot): # 고른 값 없이 테마 변수만(색 고정값 없음)
- assert "#" not in spec and "rgb(" not in spec
-
-
-def test_밝음_어두움_두_색이_다르고_글_대비_유지():
- _, head, foot = _tokens()
- for dark in (False, True):
- theme = _theme_vars(dark)
- h, f = _mix(head, theme), _mix(foot, theme)
- assert sum(abs(h[i] - f[i]) for i in range(3)) >= 40, (dark, h, f) # 서로 다른 색
- card = _rgb("--color-surface-raised", theme)
- assert sum(abs(f[i] - card[i]) for i in range(3)) >= 40 # 카드 바탕과도 구분
- for color in ("--color-text", "--color-text-secondary", "--color-accent"):
- assert _contrast(_rgb(color, theme), h) >= 4.5, (dark, "head", color)
- for color in ("--color-text", "--color-text-body", "--color-text-secondary"):
- assert _contrast(_rgb(color, theme), f) >= 4.5, (dark, "foot", color)
-
-
-def test_어두움_테마_값():
- # 어두움 테마에서 쓰는 토큰이 실제로 어두움 값으로 덮임(밝음 값 그대로가 아님)
- light, dark = _theme_vars(False), _theme_vars(True)
- for token in ("--color-surface", "--color-surface-raised", "--color-accent"):
- assert _rgb(token, light) != _rgb(token, dark), token
diff --git a/resources/tester/test_203_b09_hint_lines.py b/resources/tester/test_203_b09_hint_lines.py
deleted file mode 100644
index 7d58bdf37..000000000
--- a/resources/tester/test_203_b09_hint_lines.py
+++ /dev/null
@@ -1,185 +0,0 @@
-"""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"
"
- ""
- )
- js = _module_js()
- with sync_playwright() as p:
- browser = p.chromium.launch(channel="msedge", headless=True)
- pg = browser.new_page(viewport={"width": 1280, "height": 900})
- pg.set_content(html)
- pg.add_script_tag(
- type="module",
- content=f"{js}\nwindow.dt = {{ createDataTable }};",
- )
- pg.wait_for_function("!!window.dt")
- yield pg
- browser.close()
-
-
-@pytest.mark.parametrize("lines", [3, 2])
-def test_줄_많음_표_창_끝_합계_줄_고정_안내_글_바닥(page, lines):
- out = page.evaluate(MEASURE, [300, lines, LONG])
- assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out # 높이 = 줄 수 × 줄 높이
- assert out["hintScrolls"], out # 넘친 글 = 영역 안 스크롤
- assert out["tableScrolls"], out
- assert out["page"] == out["vh"], out # 페이지 스크롤 0
- assert out["hintBottom"] + out["padB"] == pytest.approx(out["vh"], abs=1.5), out # 창 끝까지 씀
- assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 바로 아래
- assert out["tableBottom"] - 30 < out["footBottom"] <= out["tableBottom"], (
- out
- ) # 합계 줄 = 표 칸 바닥
-
-
-def test_줄_수_2_이면_표가_한_줄만큼_더_김(page):
- three = page.evaluate(MEASURE, [300, 3, LONG])
- two = page.evaluate(MEASURE, [300, 2, LONG])
- assert two["maxH"] - three["maxH"] == pytest.approx(three["lh"], abs=1), (two, three)
-
-
-@pytest.mark.parametrize("lines", [3, 2])
-def test_줄_적음_합계_줄_안내_글_끌려_올라옴(page, lines):
- out = page.evaluate(MEASURE, [3, lines, LONG])
- assert not out["tableScrolls"], out
- assert out["tableBottom"] - out["tableTop"] < out["maxH"] - 100, out # 내용만큼(창 끝 아님)
- assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 끝에 붙음
- assert out["hintBottom"] < out["vh"] - 300, out # 화면 바닥에 떨어져 있지 않음
- assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out
- assert out["footBottom"] <= out["tableBottom"], out
-
-
-def test_짧은_글은_한_줄만(page):
- out = page.evaluate(MEASURE, [300, 3, SHORT])
- assert out["hintH"] == pytest.approx(out["lh"], abs=0.5), out
- assert not out["hintScrolls"], out
- assert out["page"] == out["vh"], out
-
-
-def test_spaceBelow_형제_바깥_여백_셈():
- src = _read(UI / "ui_template_data_table.ts")
- assert "px(m.marginTop) + px(m.marginBottom)" in src # 브라우저 배치는 위 시험이 잼
diff --git a/resources/tester/test_205_1_structure_actions_height.py b/resources/tester/test_205_1_structure_actions_height.py
deleted file mode 100644
index 9d3ae79b4..000000000
--- a/resources/tester/test_205_1_structure_actions_height.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""205-1 「구조물 배치」 맨 아래 [추가] · [삭제] · [리셋] 줄 — 단추 높이 = 상자 안 작은 단추 · 늘 보임.
-
-- 높이 = `--b05-control-h`(상자 안 단추 · 입력 칸과 같은 값) · 상자 이름표 [삭제]와 같은 높이 식
-- 줄 = 스크롤 영역 아래 끝에 붙음(sticky · bottom 0) · 단추 · 줄에 hidden · display none · visibility ·
- 호버 조건이 없음
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-CSS = (ROOT / "B05_Profile/B05_Profile_UI_Style_Structures.css").read_text(encoding="utf-8")
-FORM = (ROOT / "B05_Profile/B05_Profile_UI_Structures_Form.ts").read_text(encoding="utf-8")
-SECTION = ".b05-structure-section .b05-route__irregular-actions"
-
-
-def _block(selector: str) -> str:
- m = re.search(r"(?m)^" + re.escape(selector) + r"\s*\{([^}]*)\}", CSS)
- assert m, selector
- return m.group(1)
-
-
-def _decl(block: str, prop: str) -> str:
- m = re.search(r"(?:^|;|\s)" + re.escape(prop) + r"\s*:\s*([^;]+);", block)
- assert m, prop
- return m.group(1).strip()
-
-
-def test_buttons_same_height_as_inner_buttons():
- outer = _block(SECTION + " > button")
- inner = re.search(r"\.b05-structure__group > legend > button\s*\{([^}]*)\}", CSS).group(1)
- assert _decl(outer, "height") == "var(--b05-control-h)"
- for prop in ("height", "box-sizing", "padding-block", "font-size", "line-height"):
- assert _decl(outer, prop) == _decl(inner, prop), prop
- assert re.search(r"--b05-control-h:\s*26px;", CSS)
-
-
-def test_row_always_visible():
- row = _block(SECTION)
- assert _decl(row, "position") == "sticky"
- assert _decl(row, "bottom") == "0"
- assert "background" in row
- for text in (_block(SECTION), _block(SECTION + " > button")):
- assert not re.search(r"display\s*:\s*none|visibility|opacity|overflow", text)
- assert not re.search(re.escape(SECTION) + r"[^{]*(:hover|:focus-within|\[hidden\])", CSS)
-
-
-def test_form_never_hides_buttons():
- assert "removeButton.hidden" not in FORM and "resetButton.hidden" not in FORM
- assert "actions.hidden" not in FORM
diff --git a/resources/tester/test_217_1_b06_notice_spacing.py b/resources/tester/test_217_1_b06_notice_spacing.py
deleted file mode 100644
index e15d8ef74..000000000
--- a/resources/tester/test_217_1_b06_notice_spacing.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""217-1 B06 알림 칩 줄 여백 줄임 — 줄 위 여백 16→8 · 칩 줄 ↔ 첫 카드 28→16 (CSS 값 고정)."""
-
-from test_196_1_b06_notice_chips import B06
-
-CSS = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")
-
-
-def _rule(selector: str) -> str:
- body = CSS[CSS.index(selector + " {") :]
- return body[: body.index("}")]
-
-
-def test_칩_줄_위_여백은_8_아래_여백은_0():
- bar = _rule(".b06-section__notice-bar")
- assert "padding-block: var(--spacing-8) 0;" in bar
- assert "spacing-16) var(--spacing-4)" not in bar
-
-
-def test_첫_카드와의_간격은_카드_칸_gap_24에서_8을_당겨_16():
- bar = _rule(".b06-section__notice-bar")
- # 위 = 카드 칸 padding(16) 상쇄 · 아래 = gap(24) 중 8 당김
- assert "margin-block: calc(-1 * var(--spacing-16)) calc(-1 * var(--spacing-8));" in bar
- assert "gap: var(--spacing-24)" in _rule(".b06-section__cards")
-
-
-def test_sticky_top_과_scroll_margin_그대로():
- bar = _rule(".b06-section__notice-bar")
- assert "position: sticky" in bar
- assert "top: calc(-1 * var(--spacing-16))" in bar
- assert "scroll-margin-top: calc(var(--b06-notice-h, 0px) + var(--spacing-8))" in CSS
diff --git a/resources/tester/test_229_1_b09_level_buttons_subbar.py b/resources/tester/test_229_1_b09_level_buttons_subbar.py
deleted file mode 100644
index 6d3d1a40a..000000000
--- a/resources/tester/test_229_1_b09_level_buttons_subbar.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""229-1 B09 설계내역서 레벨 단추 = 보조 탭 줄 오른쪽 — 단추 넷 · 다른 탭엔 없음 · 표 위 도구 줄 없음.
-
-- 단추 = 보조 탭 줄 도구 자리(`setTools`) · 안내 글 = 표 아래 · 못 하는 동작 = 흐림(200-2 그대로)
-- 쉘 배선 = 소스 글 대조(test_200_2 · test_202_1 과 같은 꼴)
-"""
-
-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 TABS, run # noqa: E402
-
-B09 = "B09_Estimation/B09_Estimation_UI_"
-FILES = [TABS, f"{B09}TabBar.ts", f"{B09}Tabs_Bill_Level.ts"]
-# 가짜 DOM 에 줄 찾기(closest · querySelector(All))만 더함 · 끌기 = 빈 함수
-PRELUDE = r"""
-document.addEventListener = () => {};
-const attachDrag = () => {};
-E.prototype.closest = function () { for (let e = this; e; e = e.parent) if (e.tagName === "TR") return e; return null; };
-E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-focus")); };
-E.prototype.querySelector = function (s) {
- const i = s.match(/data-i="(-?\d+)"/)?.[1];
- return this.children.find((c) => c.dataset.i === i) ?? null;
-};
-"""
-
-
-def test_설계내역서_보조_줄에_단추_넷_다른_탭엔_없음():
- out = run(
- FILES,
- """
-const bars = createTabBars(() => {});
-const items = ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"].map((t) => [t, () => {}]);
-const table = { rows: [{ key: "r1" }], menu: (i) => (i === 0 ? items.slice(1, 3) : []) };
-const root = document.createElement("div");
-const tr = document.createElement("tr");
-tr.dataset.i = "0";
-root.append(tr);
-const toolsOf = () => bars.sub.children[bars.sub.children.length - 1];
-const snap = (lv) => ({
- labels: toolsOf().children.map((b) => b.textContent),
- levels: toolsOf().children.map((b) => b.dataset.b09Level ?? null),
- guideIn: lv ? lv.guide.textContent.length > 0 : null,
-});
-const show = (tab) => {
- bars.draw(tab);
- bars.setTools([]);
- if (tab !== "bill") return null;
- const lv = levelBar(root, table);
- bars.setTools(lv.tools);
- return lv;
-};
-const bill = show("bill");
-const a = snap(bill);
-const disabledBefore = bill.tools.map((b) => b.disabled);
-tr.click();
-const disabledAfter = bill.tools.map((b) => b.disabled);
-show("dg");
-const b = snap(null);
-show("bill");
-const c = snap(null);
-console.log(JSON.stringify({
- a, disabledBefore, disabledAfter, b: b.labels, c: c.labels,
- guideTag: bill.guide.tagName, guideCls: bill.guide.className, subBlank: bars.sub.classList.contains("b09-estimation__tabs--blank"),
-}));
-""",
- PRELUDE,
- )
- names = ["→ 들여쓰기", "← 내어쓰기", "↑ 위로", "↓ 아래로"]
- assert out["a"]["labels"] == names # 설계내역서 = 단추 넷(보조 줄 오른쪽 도구 자리)
- assert out["a"]["levels"] == ["들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"]
- assert out["a"]["guideIn"] is True # 안내 글 = 단추 줄이 아니라 따로(표 아래로)
- assert out["guideTag"] == "P" and "ui-general-block__empty" in out["guideCls"]
- assert not out["subBlank"] # 257-1 설계내역 = 아래 탭 둘(총괄 · 설계내역서) · 빈 보조 줄 아님
- assert out["disabledBefore"] == [True] * 4 # 고른 줄 없음 = 흐림
- assert out["disabledAfter"] == [True, False, False, True] # 줄 누름 = 메뉴 항목대로
- assert out["b"] == [] # 다른 탭(일위대가표) = 단추 없음
- assert out["c"] == names # 설계내역서로 돌아오면 다시 있음
-
-
-def test_표_위_도구_줄_없음_쉘_배선():
- shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert "if (level) bars.setTools(level.tools);" in shell
- assert (
- "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell
- ) # 203 레벨 안내 + 탭 안내 = 한 영역
- assert "...(level ? [level] : [])" not in shell # 표 위 자리 없음
- css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- lvl = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8")
- assert "b09-estimation__level" not in css and "b09-estimation__level" not in lvl
- # 키 · 단추 = 우클릭 메뉴 같은 항목 · 키 안 바뀜(Tab · Shift+Tab · Alt+↑↓ · 방향키)
- assert re.search(r"ACTS\[e\.shiftKey \? 1 : 0\]\[1\]", lvl)
diff --git a/resources/tester/test_236_1_hint_one_line.py b/resources/tester/test_236_1_hint_one_line.py
deleted file mode 100644
index df3f087c5..000000000
--- a/resources/tester/test_236_1_hint_one_line.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""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
diff --git a/resources/tester/test_240_1_b09_bill_folder_font_drag.py b/resources/tester/test_240_1_b09_bill_folder_font_drag.py
deleted file mode 100644
index ec2b4564f..000000000
--- a/resources/tester/test_240_1_b09_bill_folder_font_drag.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""PLAN 240 · 121 · 122 — B09 설계내역서 폴더 줄 글꼴 한 벌 · 들여쓰기 폭 · 끌기 미리 보기 · 데이터 점검 틈."""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-B09 = ROOT / "B09_Estimation"
-
-
-def _read(p: Path) -> str:
- return p.read_text(encoding="utf-8")
-
-
-def test_폴더_줄은_처음이든_레벨_고쳐_생겼든_같은_줄_꼴():
- src = _read(B09 / "B09_Estimation_UI_Tabs_Bill.ts")
- assert 'const title = r.kind === "item" ? "" : "is-title";' in src
- assert '"is-group"' not in src
- css = _read(B09 / "B09_Estimation_UI_Page.css")
- assert "tr.is-group" not in css # 옛 묶음 줄 꼴(색 다름) 없음
-
-
-def test_들여쓰기_한_단_넓힘_끌기_한_단과_같음():
- css = _read(ROOT / "ui_template" / "ui_template_data_table.css")
- assert re.search(r"--dt-indent: 22px;", css)
- drag = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Drag.ts")
- assert 'getPropertyValue("--dt-indent")' in drag
-
-
-def test_끌기_미리_보기_어디_안_몇_레벨_폴더_테두리_끄는_줄_흐림():
- drag = _read(B09 / "B09_Estimation_UI_Tabs_Bill_Drag.ts")
- for part in (
- "b09-estimation__drop-tag",
- "is-drop-into",
- "is-drag-src",
- "못 놓음",
- "안",
- "맨 위",
- ):
- assert part in drag, part
- css = _read(B09 / "B09_Estimation_UI_Page.css")
- for sel in (".b09-estimation__drop-tag", "tr.is-drop-into td", "tr.is-drag-src td"):
- assert sel in css, sel
- # 끝나면 표시를 모두 걷음
- assert 'into?.classList.remove("is-drop-into")' in drag
- assert 'el.classList.remove("is-drag-src")' in drag
-
-
-def test_데이터_점검_업데이트_위_틈_카드_왼쪽_여유와_같음():
- css = _read(B09 / "B09_Estimation_UI_Page.css")
- m = re.search(r"\.b09-options \[data-b09-refresh\] \{\s*margin-top: ([^;]+);", css)
- # 269 — 공용 카드 틈(8) + 4 = 카드 왼쪽 여유(12)
- assert m and m.group(1) == "var(--spacing-4)"
- overlay = _read(ROOT / "ui_template" / "ui_template_overlay.css")
- assert re.search(
- r":where\(\.ui-sidebar-card\.ui-panel-form\) \{[^}]*gap: var\(--spacing-8\);", overlay
- )
- card = _read(ROOT / "ui_template" / "ui_template_overlay.css")
- assert re.search(
- r"\.ui-sidebar-card \{\s*padding: calc\(var\(--spacing-8\) \+ var\(--spacing-4\)\);", card
- )
diff --git a/resources/tester/test_241_1_input_cell_border.py b/resources/tester/test_241_1_input_cell_border.py
deleted file mode 100644
index 4d1b2a9d6..000000000
--- a/resources/tester/test_241_1_input_cell_border.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""PLAN 241-1 · 123 → 246-1 · 128 — B08 · B09 표 입력 칸 = 테두리 되돌림(241 앞 채움 색)."""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-CSS = ROOT / "ui_template" / "ui_template_data_table.css"
-
-
-def _rule(css: str, selector: str) -> str:
- m = re.search(re.escape(selector) + r" \{([^}]*)\}", css)
- assert m, selector
- return m.group(1)
-
-
-def test_입력_칸_채움_색_테두리_없음():
- body = _rule(CSS.read_text(encoding="utf-8"), ".ui-data-table__input")
- # 246-1 — 241 앞 모양(accent 5% 채움) · 안쪽 테두리 없음
- assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body
- assert "box-shadow" not in body
- assert "#" not in body # 하드코딩 색 없음 — 테마 변수만
- assert "border: 0;" in body
- assert "height: var(--dt-row-h);" in body
-
-
-def test_초점_테두리_그대로():
- body = _rule(CSS.read_text(encoding="utf-8"), ".ui-data-table__input:focus")
- assert "outline: 2px solid var(--color-focus-ring);" in body
- assert "outline-offset: -2px;" in body
-
-
-def test_고친_값_색_칸_그대로():
- css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text(encoding="utf-8")
- body = _rule(css, ".b09-estimation .ui-data-table__table tbody td.is-edited")
- assert "color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas))" in body
- ts = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text(encoding="utf-8")
- assert '"is-edited"' in ts
diff --git a/resources/tester/test_245_1_merged_hover.py b/resources/tester/test_245_1_merged_hover.py
deleted file mode 100644
index bf19478f8..000000000
--- a/resources/tester/test_245_1_merged_hover.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""245-1 병합 표 호버 정리 — 병합 칸(is-merged)은 줄 강조(호버 · 이동해 온 줄 · 줄무늬) 밖.
-
-규칙 (가) = 병합 칸은 칠하지 않고 그 줄 나머지 칸만. 같은 세기 선택자라 병합 칸 규칙이 뒤에 와야 이김.
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-CSS = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
-MERGED = ".ui-data-table__table tbody tr td.is-merged {"
-
-
-def _at(sel: str) -> int:
- i = CSS.find(sel)
- assert i >= 0, sel
- return i
-
-
-def test_병합_칸_줄_강조_밖():
- body = CSS[_at(MERGED) :].split("}", 1)[0]
- assert "background: var(--color-canvas);" in body
- assert "box-shadow: none;" in body
-
-
-def test_호버_고른줄_줄무늬_뒤에_옴():
- at = _at(MERGED)
- for sel in (
- ".ui-data-table__table tbody tr:hover td {",
- ".ui-data-table__table tbody tr.is-focus td {",
- ".ui-data-table__table tbody tr.is-alt td {",
- ):
- assert _at(sel) < at, sel
-
-
-def test_병합_칸_글_위_정렬_그대로():
- assert re.search(r"td\.is-merged \{\s*vertical-align: top;", CSS)
-
-
-def test_B09_빨강_범위는_병합_칸에도_남음():
- page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- # 빨강(is-pick)은 .b09-estimation 이 붙어 더 셈 — 병합 칸 box-shadow: none 에 안 덮임
- assert ".b09-estimation .ui-data-table__table tbody tr.is-pick td {" in page
diff --git a/resources/tester/test_246_1_b09_bill_menu_simple.py b/resources/tester/test_246_1_b09_bill_menu_simple.py
deleted file mode 100644
index 4df6d88a6..000000000
--- a/resources/tester/test_246_1_b09_bill_menu_simple.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""246-1 · 128 — B09 설계내역서 우클릭 메뉴 줄임 · 입력 칸 테두리 되돌림.
-
-- 입력 칸 = 241 앞 모양(글 상자 accent 5% 채움 · td 표시 없음)
-- 더함 = 「항목 추가」 하나 — 항목 = 바로 아래 같은 부모 · 폴더 = 그 폴더 첫 하위
-- 맨 위 1단(새 공종) = ← 내어쓰기 — 직접비 · 손 줄은 묶음 · 관급자재대 앞 · 자재 줄은 못 냄
-- 연결 이름 = 「구조물 조합」 · 「일위대가 사전조합」 · 「단가산출」 · 지움 = 「줄 삭제」
-"""
-
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).resolve().parent))
-from test_184_3_b09_bill_menu import DG, ROWS, _menu, _order, _row # noqa: E402
-
-ROOT = Path(__file__).resolve().parents[2]
-OLD_ADDS = ["위에 줄 추가", "아래에 줄 추가", "아래에 하위 줄 추가", "새 공종 추가(1단)"]
-
-
-def test_입력_칸_채움_색_td_테두리_옅게():
- css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
- body = css.split(".ui-data-table__input {")[1].split("}")[0]
- assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body
- assert "box-shadow" not in body
- assert "td.is-can-edit {" in css # 258-2 — 고칠 칸 옅은 outline 되살림
-
-
-def test_더함_하나_줄_삭제_연결_이름_셋():
- keys = ["sec:1", "earth:4", "hand:g", "hand:a", "fee", "sec:G"]
- out = _menu({}, labels=keys)["labels"]
- for key in keys:
- assert [x for x in out[key] if "추가" in x] == ["항목 추가"], key
- assert not set(OLD_ADDS) & set(out[key]) and "줄 지우기" not in out[key]
- assert "줄 삭제" in out["earth:4"] and "줄 삭제" in out["hand:a"]
- link = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8")
- # 251-6 고르기 다섯 = 단가연결 모달 하나 · 종류 이름 = 단가산출 · 사전조합 · 구조물 조합 · 호표 둘
- kinds = link.split("kindFilter: [")[1].split("],\n pick")[0]
- for name in ["일위대가 호표", "단가산출 호표", "구조물 조합", "일위대가 사전조합", "단가산출"]:
- assert f'text: "{name}"' in kinds
- for word in ["서고", "마스터", "회사", "개인", "M01", "새 산근", "로직"]:
- assert word not in kinds
- toasts = [part.split(",")[0] for part in link.split("showToast(")[1:]]
- assert toasts and not any("서고" in t for t in toasts) # 알림 글 쉬운 말
- bill = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8")
- assert "항목 추가 · 줄 삭제" in bill # 표 아래 안내 글
-
-
-#: 관급자재대 안 손 묶음(hand:s · 자재 없음) · 손 항목(hand:t)
-SUPPLY = [*ROWS[:-1], _row("hand:s", "sec:G", 2, "group", cost="supply"),
- _row("hand:t", "hand:s", 3, cost="supply", qty=1, ref=DG), ROWS[-1]] # fmt: skip
-
-
-def test_자리_규칙():
- runs = [("hand:g", "항목 추가"), ("hand:a", "항목 추가"), ("hand:b", "항목 추가"),
- ("hand:a", "내어쓰기 ←"), ("hand:g", "내어쓰기 ←"), ("hand:s", "내어쓰기 ←"),
- ("hand:t", "내어쓰기 ←"), ("mat:M1", "내어쓰기 ←")] # fmt: skip
- out = _menu({}, runs=runs, rows=SUPPLY)["runs"]
- new = lambda b: [(e.get("parent"), e.get("name", e["key"])) for e in b["rows"]] # noqa: E731
- # 폴더 = 그 폴더 첫 하위 · 항목 = 바로 아래 같은 부모
- got = new(out["hand:g|항목 추가"])
- assert got[got.index(("sec:1.2", "hand:g")) + 1] == ("hand:g", "새 줄")
- got = [e["key"] for e in out["hand:a|항목 추가"]["rows"]]
- row = out["hand:a|항목 추가"]["rows"][got.index("hand:a") + 1]
- assert (row["name"], row["parent"]) == ("새 줄", "hand:g")
- got = out["hand:b|항목 추가"]["rows"]
- row = got[[e["key"] for e in got].index("hand:b") + 1]
- assert (row["name"], row["parent"], "kind" in row) == ("새 줄", "sec:1.2", False)
- # ← = 한 단씩 — 레벨 4 → 3(sec:1.2 아래) · 레벨 3 → 2(sec:1 아래)
- assert ("hand:a", "sec:1.2") in _order(out["hand:a|내어쓰기 ←"])
- assert ("hand:g", "sec:1") in _order(out["hand:g|내어쓰기 ←"])
- # 관급 안 손 묶음 ← = 새 공종(직접비) · 관급자재대 앞
- b = out["hand:s|내어쓰기 ←"]
- tops = [k for k, p in _order(b) if p is None]
- assert tops == ["sec:1", "sec:4", "hand:s", "sec:G"]
- s = {e["key"]: e for e in b["rows"]}["hand:s"]
- assert (s["kind"], s["cost"]) == ("group", "direct")
- # 손 항목 ← 관급 묶음 밖(sec:G 아래) · 자재 줄 = 1단 안 됨
- assert ("hand:t", "sec:G") in _order(out["hand:t|내어쓰기 ←"])
- assert out["mat:M1|내어쓰기 ←"] is None
- # 1단 손 항목 → 묶음(수량 · 연결 지움) — 2단 손 항목을 두 번 냄
- one = _menu({"bill": out["hand:t|내어쓰기 ←"]}, runs=[("hand:t", "내어쓰기 ←")],
- rows=SUPPLY)["runs"]["hand:t|내어쓰기 ←"] # fmt: skip
- t = {e["key"]: e for e in one["rows"]}["hand:t"]
- assert ("parent" in t, t["kind"], t["qty"], t["ref"], t["cost"]) == (
- False, "group", None, None, "direct") # fmt: skip
diff --git a/resources/tester/test_248_1_b09_group_jump_unpick.py b/resources/tester/test_248_1_b09_group_jump_unpick.py
deleted file mode 100644
index 5c555a3b9..000000000
--- a/resources/tester/test_248_1_b09_group_jump_unpick.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""248-1 B09 묶음 탭(일위대가표 · 중기사용료 · 단가산출근거) 다른 탭 이동 빨강 = 설계내역서(242-1) 규칙.
-
-- 이동해 온 호표 = 빨강 하나 — 카드 = 박스 is-pick 만(줄 is-focus 안 붙음) · 표 = 구간 is-pick 만 ·
- 공용 focusRow(at, false) = 스크롤만
-- 줄 · 카드 누름 · 방향키 · Esc = 빨강 풂 · 누른 줄만 고름(is-focus) · Esc = 고름도 풂 · 다시 그려도 그대로
-- 글 칸 안 키 · 다른 키 = 그대로
-"""
-
-import json
-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
-from resources.tester.test_171_3_b09_group_tabs import DG # noqa: E402
-from resources.tester.test_181_2_b09_box_pick import ROWS # noqa: E402
-
-LABOR = "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts"
-INPUT = "{ closest: (s) => (s.includes('input') ? {} : null) }" # 글 칸 안 키
-
-
-def test_카드_빨강_하나_누름_키_풂():
- out = run(
- [COMMON, TABS, LABOR, GROUPS],
- f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
- "\nconst o = { tab: 'dg', spec, hint: null, focus: 'DG1|1', goto: () => {}, redraw: () => {},"
- " setTools: () => {}, makeTable: () => null };"
- "\nconst marks = (v) => [v.all('.is-pick').length, v.all('.is-focus').map((e) => e.dataset.at ?? 'box')];"
- "\nconst out = {};"
- "\nlet [v] = renderGroupPanel(o); out.came = marks(v);"
- "\nv.querySelector('[data-at=\"2\"]').click(); out.line = marks(v);"
- "\n[v] = renderGroupPanel(o); v.querySelector('.ui-group-card').click(); out.box = marks(v);"
- "\n[v] = renderGroupPanel(o); key('a'); key('ArrowDown', "
- + INPUT
- + "); out.kept = marks(v);"
- "\nkey('ArrowDown'); out.arrow = marks(v);"
- "\n[v] = renderGroupPanel(o); v.querySelector('[data-at=\"2\"]').click(); key('Escape'); out.esc = marks(v);"
- "\nconsole.log(JSON.stringify(out));",
- RAW,
- )
- assert out["came"] == [1, []] # 박스 빨강만 · 줄 강조 없음
- assert out["line"] == [0, ["2"]] # 누른 줄만 고름
- assert out["box"] == [0, []] # 카드 빈 곳 누름 = 빨강만 풂
- assert out["kept"] == [1, []] # 다른 키 · 글 칸 안 = 그대로
- assert out["arrow"] == [0, []]
- assert out["esc"] == [0, []] # Esc = 고름도 풂
-
-
-def test_표_빨강_하나_누름_키_풂():
- out = run(
- [COMMON, TABS, GROUPS],
- f"\nconst spec = {{ columns: [{{ key: 'key' }}], rows: {json.dumps(ROWS)} }};"
- "\nlet extra = null; let root = null; const scrolled = [];"
- "\nconst makeTable = (e) => { extra = e; root = document.createElement('table');"
- " spec.rows.forEach((_, i) => { const tr = document.createElement('tr'); tr.dataset.i = String(i);"
- " tr.className = e(i) ?? ''; tr.append(document.createElement('td')); root.append(tr); });"
- " return { root, focusRow: (i, mark) => scrolled.push([i, mark]) }; };"
- "\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {},"
- " setTools: () => {}, makeTable };"
- "\nchooseGroupView('table');"
- "\nconst marks = () => [root.children.map((tr) => tr.className || null),"
- " spec.rows.map((_, i) => extra(i))];"
- "\nconst out = {};"
- "\nrenderGroupPanel(o); out.came = marks(); out.scrolled = [...scrolled];"
- "\nroot.children[1].children[0].click(); out.click = marks();"
- "\nrenderGroupPanel(o); key('Enter'); key('ArrowUp', " + INPUT + "); out.kept = marks()[0];"
- "\nkey('ArrowUp'); out.arrow = marks();"
- "\nrenderGroupPanel(o); root.children[5].click(); key('Escape'); out.esc = marks();"
- "\nconsole.log(JSON.stringify(out));",
- )
- red = [None] * 3 + ["is-pick is-pick-top", "is-pick", "is-pick", "is-pick is-pick-end", None]
- none = [None] * 8
- assert out["came"] == [red, red] # 구간 빨강만
- assert out["scrolled"] == [[4, False]] # 스크롤만(공용 강조 안 붙임)
- focus1 = [None, "is-focus"] + [None] * 6
- assert out["click"] == [focus1, focus1] # 누른 줄만 · 다시 그려도
- assert out["kept"] == red
- assert out["arrow"] == [none, none]
- assert out["esc"] == [none, none]
-
-
-def test_배선():
- groups = (ROOT / GROUPS).read_text("utf-8")
- assert "table.focusRow(at, false)" in groups
- common = (ROOT / COMMON).read_text("utf-8")
- assert 'el?.classList.add("is-focus")' not in common
- assert "unpickCards(grid, true);" in common
diff --git a/resources/tester/test_255_3_b08_milestone_place_modal.py b/resources/tester/test_255_3_b08_milestone_place_modal.py
deleted file mode 100644
index 244906939..000000000
--- a/resources/tester/test_255_3_b08_milestone_place_modal.py
+++ /dev/null
@@ -1,42 +0,0 @@
-"""255-3 거리이정표 거래처 · 기준점 모달 — 칸 이름 ≥ 내용 글 · 「소재지 — 주소 찾기 · 지도에서 고르기」
-줄 지움 · 기준점 이름 한 줄(빈 칸 자리 비움) · 주소 · 지도 위 가로선 ·
-256-1 장소 그림 = 등록 · 좌표 바뀜 · 경로 탭 끌기 뒤 capturePlace."""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-
-
-def _read(rel):
- return (ROOT / rel).read_text(encoding="utf-8")
-
-
-def _rule(css, selector):
- m = re.search(re.escape(selector) + r" \{([^}]*)\}", css)
- assert m, selector
- return m.group(1)
-
-
-def test_모달_꼴():
- modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
- assert "주소 찾기 · 지도에서 고르기" not in modal and 'el("h4"' not in modal
- assert (
- 'item.root.style.display = dest.root.style.display = k === "base" ? "none" : "";' in modal
- )
- css = _read("B08_Quantity/B08_Quantity_UI_Milestone.css")
- label = _rule(css, ".b08-milestone__modal .ui-field__label")
- assert "font-size: var(--text-body-sm);" in label # 입력 글(text-body-sm)과 같음
- assert "white-space: nowrap;" in label
- assert "border-top: 1px solid var(--color-border);" in _rule(css, ".b08-milestone__place-field")
- place = _read("B08_Quantity/B08_Quantity_UI_Milestone_Place.ts")
- assert 'className: "b08-milestone__place-field"' in place
-
-
-def test_장소_그림_캡처_부름():
- modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
- assert modal.count("segment = placed = ") == 2 # 기준점 · 거래처 좌표 바뀜
- assert "placed = addRoute?.({" in modal and "id: placed," in modal # 새 거래처 · 새 기준점
- assert "void capturePlace(projectId, placed, {" in modal
- route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts")
- assert "void capturePlace(projectId, key, { name: target.name || label, lon, lat });" in route
diff --git a/resources/tester/test_257_2_b09_pick_blank_grand.py b/resources/tester/test_257_2_b09_pick_blank_grand.py
deleted file mode 100644
index ef5fdd736..000000000
--- a/resources/tester/test_257_2_b09_pick_blank_grand.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""257-2 B09 이동해 온 줄 = 모든 탭 빨강 상자 · 비고 「빈 줄 N」 없음 · 총공사비 = 표 아래 고정 줄.
-
-- 쉘 = 설계내역서 밖 · 묶음 아닌 탭에도 줄 범위 [at, at] 빨강(pickClass) · 줄 누름 · Esc · 방향키로 풂(unpickOnRelease)
-- 비고 = 제목 줄 「(제외금액)」만 · 묶음 줄 빈칸(Py · TS 짝 · blank 수는 줄 값 그대로)
-- 총괄 · 원가계산서 총공사비(grand) = `TabTable.total`(몸 줄에서 빠짐 · 줄 이동 누름 없음 — 185-1 시험이 값 확인)
-"""
-
-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 = ROOT / "B09_Estimation"
-
-
-def test_쉘_모든_탭_빨강_배선():
- shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert "!bill && at >= 0 ? [at, at] : null" in shell
- assert "pickClass(range, i)" in shell and "unpickOnRelease(table.root)" in shell
- assert "table.focusRow(at, false)" in shell
-
-
-def test_풂_줄_누름_Esc_빨강만_지움():
- out = run(
- [COMMON, TABS, GROUPS],
- """
-const root = document.createElement("div");
-const rows = [0, 1, 2].map((i) => {
- const tr = document.createElement("tr");
- tr.dataset.i = String(i);
- if (i === 1) tr.classList.add("is-pick", "is-pick-top", "is-pick-end");
- root.append(tr);
- return tr;
-});
-unpickOnRelease(root);
-const before = rows.map((t) => t.className);
-key("Escape");
-const afterEsc = rows.map((t) => t.className);
-rows[1].classList.add("is-pick");
-root.click();
-const afterClick = rows.map((t) => t.className);
-console.log(JSON.stringify({ before, afterEsc, afterClick }));
-""",
- RAW
- + """
-E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-pick")); };
-""",
- )
- assert out["before"] == ["", "is-pick is-pick-top is-pick-end", ""]
- assert out["afterEsc"] == ["", "", ""]
- assert out["afterClick"] == ["", "", ""]
-
-
-def test_비고_빈_줄_없음_Py_TS_짝():
- py = (B09 / "B09_Estimation_Calc_Bill.py").read_text("utf-8")
- ts = (B09 / "B09_Estimation_Calc_Bill.ts").read_text("utf-8")
- assert "빈 줄 {" not in py and not re.search(r"빈 줄 \$\{", ts)
- assert 'row["note"] = "(제외금액)" if row["key"] == "sec:G" else ""' in py
- assert 'row.note = row.key === "sec:G" ? "(제외금액)" : ""' in ts
diff --git a/resources/tester/test_257_3_b09_compare_applied_menu.py b/resources/tester/test_257_3_b09_compare_applied_menu.py
deleted file mode 100644
index c631ea294..000000000
--- a/resources/tester/test_257_3_b09_compare_applied_menu.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""257-3 B09 자재단가대비표 적용 단가 — 초록 테두리 · 우클릭 「적용 단가 바꾸기」(출처 목록).
-
-- 고른 출처 단가 칸 = 초록 테두리(inset box-shadow · 바탕 색 아님)
-- 우클릭 = 적용 단가 칸 · 고른 출처 단가 칸 → 「적용 단가 바꾸기」 하위 목록(왼쪽 누름 메뉴와 같은 항목 · 최저가여도 바꿈)
-- 왼쪽 누름 메뉴(choices)는 그대로
-"""
-
-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 TABS, run # noqa: E402
-
-BASE = "B09_Estimation/B09_Estimation_UI_Tabs_Base.ts"
-PRELUDE = r"""
-const SOURCES = ["관급", "물가정보", "물가자료", "유통물가", "견적"];
-const openQuotes = () => {};
-"""
-
-JS = """
-let user = {};
-const sheets = { compare: [
- { key: "m1", no: 1, name: "철근", spec: "D13", unit: "kg", lowest: 1000, lowest_from: "유통물가",
- applied: 1000, applied_from: "유통물가", pick: null,
- prices: { 관급: null, 물가정보: 1200, 물가자료: 0, 유통물가: 1000, 견적: null } },
-] };
-const t = buildCompare({ sheets, get user() { return user; }, setUser: (u) => (user = u) });
-const menu = t.cellMenu(0, "applied");
-const labels = menu[0].children.map((c) => c[0]);
-menu[0].children[0][1](); // 물가정보 = 최저가 아닌 출처로 바꿈
-const afterPick = JSON.stringify(user.materials);
-menu[0].children[1][1](); // 유통물가 = 최저가여도 고른 출처로 박음
-const afterLowest = JSON.stringify(user.materials);
-console.log(JSON.stringify({
- label: menu[0].label, labels, afterPick, afterLowest,
- onPicked: t.cellMenu(0, "p:유통물가")?.[0].label ?? null,
- other: [t.cellMenu(0, "name"), t.cellMenu(0, "p:물가정보"), t.cellMenu(5, "applied")],
- left: t.choices(0, "applied").length, leftOther: t.choices(0, "name"),
- cls: [t.cellClass(0, "p:유통물가"), t.cellClass(0, "p:물가정보")],
-}));
-"""
-
-
-def test_우클릭_적용_단가_바꾸기_출처_목록():
- out = run([TABS, BASE], JS, PRELUDE)
- assert out["label"] == "적용 단가 바꾸기"
- assert out["labels"] == ["물가정보 1,200", "✓ 유통물가 1,000 · 최저", "✓ 최저(자동)"]
- assert out["afterPick"] == '{"m1":{"pick":"물가정보"}}'
- assert out["afterLowest"] == '{"m1":{"pick":"유통물가"}}' # 최저가여도 바꿈
- assert out["onPicked"] == "적용 단가 바꾸기" # 초록 테두리 칸 우클릭도 같은 메뉴
- assert out["other"] == [None, None, None]
- assert (
- out["left"] == 4 and out["leftOther"] is None
- ) # 왼쪽 누름 메뉴 그대로(출처 둘 · 구분선 · 최저)
- assert out["cls"] == ["is-picked", None]
-
-
-def test_배선_쉘_우클릭_CSS_초록_테두리():
- shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
- assert 'made.root.addEventListener("contextmenu"' in shell and "spec.cellMenu" in shell
- assert "openMenuAt(event.clientX, event.clientY, items)" in shell
- css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
- box = css.split("td.is-picked {", 1)[1].split("}", 1)[0]
- assert "box-shadow: inset 0 0 0 2px var(--color-success)" in box and "background" not in box
diff --git a/resources/tester/test_258_1_table_width_var.py b/resources/tester/test_258_1_table_width_var.py
deleted file mode 100644
index 7831b90d7..000000000
--- a/resources/tester/test_258_1_table_width_var.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""258-1 · 258-2 · 258-3 — 표 폭 CSS 변수 · 입력 칸 테두리 옅게 · 구조물 조합 미지정 회색(소스 시험).
-
-- 템플릿 표 인라인 = `--dt-w`(열 합) · css `width: var(--dt-w)` → 페이지 css 가 width 로 덮음
-- 거리이정표 표 = 틀 폭 100%
-- 고칠 칸 td.is-can-edit = accent 28% outline · box-shadow 아님
-- B05 미지정 = 회색(text-muted)
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-DT_CSS = ROOT / "ui_template" / "ui_template_data_table.css"
-DT_TS = ROOT / "ui_template" / "ui_template_data_table.ts"
-
-
-def _rule(css: str, selector: str) -> str:
- m = re.search(re.escape(selector) + r" \{([^}]*)\}", css)
- assert m, selector
- return m.group(1)
-
-
-def test_표_폭_인라인_변수_css_width_변수():
- ts = DT_TS.read_text("utf-8")
- assert '' in ts
- assert 'style="width:${x}px"' not in ts # 인라인 width 가 페이지 css 를 이기지 않음
- body = _rule(DT_CSS.read_text("utf-8"), ".ui-data-table__table")
- assert "width: var(--dt-w);" in body
- assert "table-layout: fixed;" in body
-
-
-def test_거리이정표_표_틀_폭_전체():
- css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone.css").read_text("utf-8")
- body = _rule(css, ".b08-milestone > .ui-data-table .ui-data-table__table")
- assert "width: 100%;" in body # 변수 규칙(0,1,0)보다 선택자가 구체적
-
-
-def test_입력_칸_테두리_옅게_outline():
- css = DT_CSS.read_text("utf-8")
- body = _rule(css, ".ui-data-table__table td.is-can-edit")
- m = re.search(r"var\(--color-accent\) (\d+)%", body)
- assert m and 25 <= int(m.group(1)) <= 30 # 예전 60% → 옅게
- assert "outline: 1px solid" in body and "box-shadow" not in body
- assert '"is-can-edit"' in DT_TS.read_text("utf-8")
-
-
-def test_구조물_조합_미지정_회색():
- css = (ROOT / "B05_Profile" / "B05_Profile_UI_Style_Structures.css").read_text("utf-8")
- body = _rule(css, ".b05-structure__summary-name:not(.is-assigned)")
- assert "border-color: var(--color-text-muted);" in body
- assert "--color-text-muted" in body.split("background:")[1]
- assert "--color-warning" not in body
diff --git a/resources/tester/test_258_b06_plan_peek.py b/resources/tester/test_258_b06_plan_peek.py
deleted file mode 100644
index d5e12010a..000000000
--- a/resources/tester/test_258_b06_plan_peek.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""258 · 425 · 436 — B06 위 평면 겹침 패널.
-
-종단 보기 평면 노드를 옮겨 붙임(둘째 캔버스 없음) · 닫을 때 · 횡단 숨김 때 제자리 · 손잡이 =
-공용 bottom 위로 접힘 · z 4 · 처음 높이 32 % · 아래 끝 끌어 높이(화면 기억) · 단추 줄 · 안내 등 감춤 ·
-조작(끌기 · 끼우기 · 지우기 · 방향키 · 휠 · 팬)은 B05 평면 그대로 · 처음 보일 때 B05 칸 크기로 셈.
-배선은 글로 봄.
-./venv/Scripts/python.exe -m pytest resources/tester/test_258_b06_plan_peek.py -q
-"""
-
-from __future__ import annotations
-
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-DESIGN = ROOT / "A00_Common" / "design"
-PEEK = (DESIGN / "design_plan_peek.ts").read_text(encoding="utf-8")
-CROSS = (DESIGN / "design_view_cross.ts").read_text(encoding="utf-8")
-CSS = (DESIGN / "design_page.css").read_text(encoding="utf-8")
-B05 = ROOT / "B05_Profile"
-
-
-def _rule(selector: str) -> str:
- body = CSS[CSS.index(selector + " {") :]
- return body[: body.index("}")]
-
-
-def test_횡단_보기가_패널을_세움() -> None:
- assert 'import { mountPlanPeek } from "./design_plan_peek";' in CROSS
- assert CROSS.index("center.append(mainArea);") < CROSS.index(
- "mountPlanPeek(center, host, shared);"
- )
-
-
-def test_평면_노드를_옮기고_제자리로() -> None:
- assert 'createWorkflowPanelHandle("bottom", "up", "평면")' in PEEK
- assert 'await shared.ensureView("profile");' in PEEK # B06 바로 열기 = 종단 보기 숨긴 채
- assert '".b05-route__main > .b05-routeedit"' in PEEK
- assert "plan.replaceWith(home);" in PEEK and "body.append(plan);" in PEEK
- assert "home.replaceWith(plan);" in PEEK # 닫기 = 자리표 자리로
- assert "host.onHide(close);" in PEEK # 횡단 숨김(B05 로 감) = 제자리
- assert 'createElement("canvas")' not in PEEK
- assert len(PEEK.splitlines()) <= 700
-
-
-def test_조작은_B05_평면_그대로() -> None:
- # 패널이 누름 · 휠을 가로채지 않음 — 캔버스에 붙은 B05 처리기가 그대로 받음
- for event in ('"pointerdown"', '"dblclick"', '"contextmenu"', '"wheel"', '"keydown"'):
- assert event not in PEEK
- assert "stopPropagation" not in PEEK
-
-
-def test_높이_끌기와_기억() -> None:
- assert "createPanelResizer({" in PEEK
- assert 'cssVar: "--design-plan-peek-height",' in PEEK and "direction: 1," in PEEK
- assert 'storageKey: stateKey("plan-peek-height")' in PEEK
- assert "max: () => (root.offsetParent?.clientHeight ?? 0) - 48," in PEEK
- assert "resizer.restore();" in PEEK[PEEK.index("const open = async") :]
- state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8")
- assert '"plan-peek-height": { bucket: "pref", scope: "global" },' in state
- handle = _rule(".design-plan-peek > .ui-resizer")
- assert "bottom: -3px;" in handle and "display: none;" in handle
- assert "display: block;" in _rule(".design-plan-peek.is-open > .ui-resizer")
-
-
-def test_겹침_자리와_감춤() -> None:
- root = _rule(".design-plan-peek")
- assert "position: absolute;" in root and "z-index: 4;" in root
- assert "pointer-events: none;" in root # 닫힌 동안 카드 누름 통과
- assert "height: var(--design-plan-peek-height, max(32%, 160px));" in _rule(
- ".design-plan-peek.is-open"
- )
- hidden = CSS[CSS.index(".design-plan-peek .b05-routeedit__tools,") :]
- hidden = hidden[: hidden.index("}")]
- for part in ("__tools", "__info", "__measure", "__side-wrap", "b05-contourlock"):
- assert part in hidden
- assert "display: none;" in hidden
-
-
-def test_평면_방향키는_마지막_누름이_평면_안() -> None:
- for name in ("B05_Profile_UI_RouteEdit_Keys.ts", "B05_Profile_UI_RouteEdit_StructureMarks.ts"):
- text = (B05 / name).read_text(encoding="utf-8")
- assert 'lastPointerWithin(".b05-routeedit")' in text # 겹침 평면도(436)
- assert ".b05-route__main .b05-routeedit" not in text
-
-
-def test_처음_보일_때_B05_칸_크기로_셈() -> None:
- editor = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
- body = editor[editor.index("function openView(): void {") :]
- body = body[: body.index("\n }")]
- assert 'const atHome = overlay.closest(".b05-route__main") !== null;' in body
- assert 'atHome ? null : overlay.closest(".design-page__center")' in body
- assert "openingView(projectId, meta, home," in body
- assert "restoreSavedView(meta, view, captureView(meta, opening.view, 0))" in body
diff --git a/resources/tester/test_262_1_b08_milestone_title_start_pin.py b/resources/tester/test_262_1_b08_milestone_title_start_pin.py
deleted file mode 100644
index c002f4175..000000000
--- a/resources/tester/test_262_1_b08_milestone_title_start_pin.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""262 B08 거리이정표 상자 창 제목 · 소재지 지도 시점 표지(사용자 164-1).
-
-262-1 거래처 · 기준점 창 제목 = 이름만(빈 이름 = 「거래처」 · 「기준점」) · 시점 창 = 「시점 · 중점」 · 요소 추가 창 그대로
-262-2 소재지 지도 = 시점 표지 고정(끌기 없음 · 맞춤에서 빠짐) · 시점 없으면 표지 없음 · 소재지 없으면 시점 중심
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-B08 = "B08_Quantity/B08_Quantity_UI_Milestone"
-
-
-def _read(name):
- return (ROOT / f"{B08}{name}").read_text(encoding="utf-8")
-
-
-def test_제목_이름만():
- modal = _read("_Modal.ts")
- title = modal[modal.index("const title = target.add") : modal.index("openModal({")]
- assert '? base.name || "기준점"' in title and ': route!.name || "거래처"' in title
- assert '"요소 추가 — 기준점 · 거래처"' in title
- assert 'title: "시점 · 중점",' in modal and "공사지 —" not in modal
- assert re.search(r"title: `?[^\n]*(거래처|기준점|공사지) —", modal) is None
-
-
-def test_시점_표지_고정():
- place = _read("_Place.ts")
- assert "startPoint(liveMilestone(projectId))" in place # 거리이정표가 쓰는 시점
- assert "center: start" in place # 소재지 없음 = 시점 중심
- assert "movable: false, fixed: true" in place and 'name: "시점"' in place
- assert "...(s" in place and " : [])," in place # 시점 없음 = 표지 없음
- kakao = _read("_KakaoMap.ts")
- assert "fixed?: boolean;" in kakao
- assert "const moving = pins.filter((p) => !p.fixed);" in kakao
- assert "...moving.map(" in kakao # 처음 보기 = 소재지 중심(시점은 맞춤에서 빠짐)
- assert "moving.find(" in kakao # 보기 이동에서도 빠짐
- assert "if (p.fixed)" in kakao and "b08-milestone__map-start" in kakao
- css = _read(".css")
- assert ".b08-milestone__map-start {" in css
diff --git a/resources/tester/test_267_1_pipe_info_font.py b/resources/tester/test_267_1_pipe_info_font.py
deleted file mode 100644
index d819bb537..000000000
--- a/resources/tester/test_267_1_pipe_info_font.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-"""PLAN 267-1 — B06 횡단 카드 「관매설 …」 글 크기 = 「노폭 …」 글 크기 · 줄 겹침 없음.
-
-./venv/Scripts/python.exe -m pytest resources/tester/test_267_1_pipe_info_font.py -q -p no:cacheprovider --basetemp=tmp/pt
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-CSS = (ROOT / "B06_Section" / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8")
-TYPES = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Types.ts").read_text(encoding="utf-8")
-
-
-def _font_px(selector: str) -> float:
- """셀렉터 블록의 마지막 font-size(px) — 뒤에 오는 규칙이 이긴다."""
- blocks = re.findall(re.escape(selector) + r"\s*\{([^}]*)\}", CSS)
- sizes = [float(m) for b in blocks for m in re.findall(r"font-size:\s*([\d.]+)px", b)]
- assert sizes, selector
- return sizes[-1]
-
-
-def test_관_정보_글_크기가_노폭_라벨과_같다() -> None:
- road = _font_px(".b06-chart__carriageway-label")
- info = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info")
- assert road == info == 11.0
- # 다른 culvert-label(집수정 · 박스 이름)은 그대로 9px
- assert _font_px(".b06-chart__culvert-label") == 9.0
-
-
-def test_관_정보_글_줄이_노폭_라벨과_겹치지_않는다() -> None:
- step = float(re.search(r"- (\d+) \* \(stacked \+ 1\)", TYPES).group(1))
- font = _font_px(".b06-chart__culvert-label.b06-chart__pipe-info")
- road_font = _font_px(".b06-chart__carriageway-label")
- asc, desc = 0.8, 0.2 # 글자 상자 근사 — 기준선 위 80% · 아래 20%
- road_top = 0 - asc * road_font # 노폭 라벨 기준선 y=0
- for n in (1, 2, 3): # 관 정보 글이 쌓인 줄 수
- info_y = -step * n
- info_bottom = info_y + desc * font
- assert info_bottom < road_top, (n, info_bottom, road_top)
- if n > 1: # 윗줄 바닥 < 아랫줄 윗면
- upper_bottom = -step * n + desc * font
- lower_top = -step * (n - 1) - asc * font
- assert upper_bottom < lower_top, n
diff --git a/resources/tester/test_269_panel_form.py b/resources/tester/test_269_panel_form.py
deleted file mode 100644
index 4926a7c3a..000000000
--- a/resources/tester/test_269_panel_form.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# -*- coding: utf-8 -*-
-"""269 좌측 패널 컨테이너 공용 양식 — 기준 = B05/B06 「구조물 배치」.
-
-못박는 것
- - 공용 양식 = ui_template 한 곳(`.ui-panel-form` · `.ui-panel-btn` · 카드 `createPanelCard`) ·
- 칸 높이 · 라벨 틈 토큰 = theme 한 곳 · 「구조물 배치」도 같은 토큰.
- - B07 컨테이너 = 공용 카드 · 도면 단추 = `.ui-panel-btn` · 페이지 덧칠(크기 · 글자 · 바탕) 없음.
- - B06 컨테이너(횡단 보기 설정 · 표준 횡단면 설정) = 칸 틀 `createSectionFrame` 이 공용 양식을 붙임 ·
- 단추 · 입력칸 = 공용 함수(createButton · createInputField · createSelectField) · B06 덧칠 없음.
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-
-
-def _read(rel: str) -> str:
- return (ROOT / rel).read_text(encoding="utf-8")
-
-
-THEME = _read("ui_template/ui_template_theme.css")
-OVERLAY = _read("ui_template/ui_template_overlay.css")
-COLLAPSIBLE = _read("ui_template/ui_template_collapsible.ts")
-B05_STRUCT_CSS = _read("B05_Profile/B05_Profile_UI_Style_Structures.css")
-B07_CSS = _read("B07_DesignDetail/B07_DesignDetail_UI_Style.css")
-B07_PANELS = _read("B07_DesignDetail/B07_DesignDetail_UI_Panels.ts")
-B07_PLAN = _read("B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts")
-DESIGN_PANEL = _read("A00_Common/design/design_panel.ts")
-B06_CSS = _read("B06_Section/B06_Section_UI_Style.css")
-B06_PANEL = _read("B06_Section/B06_Section_UI_Page_Panel.ts")
-B06_STD = _read("B06_Section/B06_Section_UI_Standard_Panel.ts")
-
-
-def _block(css: str, selector: str) -> str:
- m = re.search(r"(?:^|\n)" + re.escape(selector) + r"\s*\{([^}]*)\}", css)
- assert m, selector
- return m.group(1)
-
-
-def test_공용_양식_토큰은_theme_한_곳_구조물_배치도_같은_토큰():
- assert "--panel-control-h: 26px;" in THEME
- assert "--panel-label-gap: 2px;" in THEME
- assert "--b05-control-h: var(--panel-control-h);" in B05_STRUCT_CSS
- assert "--b05-label-gap: var(--panel-label-gap);" in B05_STRUCT_CSS
-
-
-def test_공용_양식_칸_라벨_단추_규칙():
- field = _block(
- OVERLAY,
- '.ui-panel-form input:not([type="checkbox"], [type="radio"], [type="range"]),\n'
- ".ui-panel-form select",
- )
- assert "height: var(--panel-control-h);" in field
- assert "padding: 0 6px;" in field
- assert "font-size: var(--text-caption);" in field
- label = _block(OVERLAY, ".ui-panel-form .ui-field,\n.ui-panel-form .b05-route__field")
- assert "gap: var(--panel-label-gap);" in label
- assert "display: none;" in _block(OVERLAY, ".ui-panel-form .ui-field__error:not(.is-visible)")
- button = _block(OVERLAY, ".ui-panel-form .ui-btn:not(.ui-help-btn, .ui-step-nav-btn)")
- # 「구조물 배치」 단추(b05-route__irregular-btn)와 같은 크기
- assert "padding: var(--spacing-8);" in button
- assert "font-size: var(--text-caption);" in button
- assert "border-radius: var(--radius-inputs);" in button
- raw = _block(OVERLAY, ":where(.ui-panel-btn)")
- for decl in (
- "padding: var(--spacing-8);",
- "border-radius: var(--radius-inputs);",
- "background: var(--color-surface);",
- "font-size: var(--text-caption);",
- ):
- assert decl in raw, decl
- # 아래 단추 묶음은 컨테이너 밖 — 공용 양식이 `.ui-sidebar-actions` 를 겨누지 않음
- form_rules = OVERLAY[OVERLAY.index("좌측 패널 컨테이너 공용 양식(269)") :]
- form_rules = form_rules[: form_rules.index("/* 카드 안에 다시 카드")]
- assert "ui-sidebar-actions {" not in form_rules and "ui-sidebar-actions ." not in form_rules
-
-
-def test_공용_카드_함수():
- assert "export function createPanelCard(" in COLLAPSIBLE
- assert "ui-collapsible ui-sidebar-section ui-sidebar-card ui-panel-form" in COLLAPSIBLE
-
-
-def test_B07_컨테이너_공용_카드_도면_단추_공용_꼴():
- assert "createPanelCard(\n `${group.label} ${items.length}`,\n isFolded(" in (
- B07_PANELS.replace("\r\n", "\n")
- )
- assert "createPanelCard(`평면도 ${items.length}`, collapsed, onManage)" in B07_PLAN
- for src in (B07_PANELS, B07_PLAN):
- assert "ui-collapsible ui-sidebar-section" not in src # 손으로 짠 카드 없음
- assert B07_PANELS.count('"b07-drawing-button ui-panel-btn"') == 2
- assert "b07-plan-manage-open" not in B07_PLAN # 280-5 [평면도 관리] 단추 → 제목 ⚙
-
-
-def test_B07_페이지_덧칠_없음():
- assert not re.search(r"(?:^|\n)\.b07-drawing-group\s*\{", B07_CSS)
- assert ".b07-drawing-group h3 {" not in B07_CSS
- assert ".b07-plan-manage-open {" not in B07_CSS
- button = _block(B07_CSS, ".b07-drawing-button")
- for prop in ("min-height", "padding", "border-radius", "background", "font-size"):
- assert not re.search(r"(?:^|\s)" + prop + r"\s*:", button), prop
- assert "font-size" not in _block(B07_CSS, ".b07-drawing-button__name")
- assert ".b07-cross-layout .ui-btn" not in B07_CSS
- assert ".b07-cross-layout .ui-field__error" not in B07_CSS
-
-
-def test_B06_컨테이너_공용_양식():
- frame = DESIGN_PANEL[DESIGN_PANEL.index("export function createSectionFrame(") :]
- frame = frame[: frame.index("\n}\n")]
- cross = frame[frame.index("b06-profile__group ui-collapsible") :]
- assert "ui-panel-form" in cross.splitlines()[0]
- for src in (B06_PANEL, B06_STD):
- assert "createSectionFrame(" in src or "createButton(" in src
- assert "" in chrome and ">기울기" in chrome
- assert '[data-act="stations"]\')!, "stations")' in chrome
- assert '[data-act="slopes"]\')!, "slopes")' in chrome
-
-
-def test_측점_끄면_눈금만_빠지고_이름표_남음():
- render = _read("B05_Profile_UI_RouteEdit_Render.ts")
- body = render[render.index("function drawStationMarks(") : render.index("function endLabel(")]
- # 라벨 상자를 먼저 비우고(누름 판정) · 눈금은 켬일 때만 · 이름표는 밖
- assert body.index("setStationLabelBoxes(") < body.index('if (markShown("stations"))')
- guard = body.index('if (markShown("stations"))')
- assert guard < body.index("drawStationTicks(") < body.index("`시점 ")
- assert body.count("endLabel(") == 2
-
-
-def test_기울기_끄면_라벨도_높이_요청도_없음():
- render = _read("B05_Profile_UI_RouteEdit_Render.ts")
- assert 'if (markShown("slopes")) scene.overStations?.(context);' in render
- assert render.count("scene.overStations?.(context)") == 1
-
-
-def test_측점_끄면_눈금_집기_없음():
- pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
- # 집기에 쓰는 눈금 찾기는 켬일 때만 — 라벨 0개면 전부 집히는 함정
- at = 0
- calls = 0
- while (at := pointer.find("stationAtScreen(", at + 1)) >= 0:
- calls += 1
- assert 'markShown("stations")' in pointer[at - 300 : at], at
- assert calls >= 1
diff --git a/resources/tester/test_381_3_routeedit_node_pick.py b/resources/tester/test_381_3_routeedit_node_pick.py
deleted file mode 100644
index edc6a58f0..000000000
--- a/resources/tester/test_381_3_routeedit_node_pick.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""381-3 B05 평면 교각점(요구 241-14) — 크기 · 그리는 차례 · 맴돔 강조 · 집기 한 길.
-
-겹침 셈(`pickAtScreen`)은 test_b05_routeedit_pick 이 돌려 봄 — 여기는 배선만.
-"""
-
-from pathlib import Path
-
-B05 = Path(__file__).resolve().parents[2] / "B05_Profile"
-
-
-def _read(name: str) -> str:
- return (B05 / name).read_text(encoding="utf-8")
-
-
-def test_노드_크기_문턱():
- assert "const NODE_R = 6;" in _read("B05_Profile_UI_RouteEdit_Render.ts")
- pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
- assert "const NODE_HIT_PX = 12;" in pointer and "const NODE_CORE_PX = 6;" in pointer
-
-
-def test_노드는_라벨_뒤_맨_위_회전_안():
- render = _read("B05_Profile_UI_RouteEdit_Render.ts")
- scene = render[render.index("export function drawRouteEditScene(") :]
- scene = scene[: scene.index("context.restore(); // 회전 끝")]
- labels = scene.index("drawContourLabels(context, scene, band, style);")
- for later in ("scene.planned.forEach(", "drawPickedCurve(", "drawPickHover("):
- assert labels < scene.index(later), later
- for earlier in ("drawStationMarks(", "scene.overStations?.(", "drawMeasureMarks("):
- assert scene.index(earlier) < labels, earlier
-
-
-def test_곡선_네모는_예상노선_뒤_노선_앞():
- """414-2 — 모든 곡선 BC · EC 네모 = 예상노선 뒤 · 노선 앞(노선이 네모 위를 지나감)."""
- render = _read("B05_Profile_UI_RouteEdit_Render.ts")
- scene = render[render.index("export function drawRouteEditScene(") :]
- expected = scene.index("scene.expected,")
- squares = scene.index("scene.curveInfo.forEach(")
- route = scene.index("strokePolyline(", squares)
- assert expected < squares < route
- assert scene[route:].split(",")[2].strip() == "line"
-
-
-def test_집기_맴돔_한_함수():
- pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
- assert "pickAtScreen(" in pointer
- down = pointer[pointer.index('addEventListener("pointerdown"') :]
- down = down[: down.index('addEventListener("pointermove"')]
- assert "hover(null);" in down and "const pick = choice.pick;" in down # chooseAt 안 pickAt(433)
- assert "nodeAt(px, py)" not in down and "stationAtScreen(" not in down
- move = pointer[pointer.index('addEventListener("pointermove"') :]
- move = move[: move.index("const endDrag")]
- # 끄는 동안은 앞에서 돌아감 — 맴돔은 끌기 갈래 뒤
- assert move.index("if (dragNode >= 0)") < move.index("chooseAt(px, py)")
- assert "hover(over);" in move
- assert 'addEventListener("pointerleave"' in pointer
-
-
-def test_맴돔_그림():
- hover = _read("B05_Profile_UI_RouteEdit_Hover.ts")
- assert "pickHover(context.canvas)" in hover
- assert (
- '"node" in hover' in hover
- and "drawStationNormal(context, scene, line, hover.chainage)" in hover
- )
- # 391-1 맴돔 = 고름 한 함수 · 좌우 20 m 고정
- assert "const STATION_NORMAL_HALF_M = 20;" in hover
diff --git a/resources/tester/test_383_3_rerun_row.py b/resources/tester/test_383_3_rerun_row.py
deleted file mode 100644
index 859aef478..000000000
--- a/resources/tester/test_383_3_rerun_row.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""383-3 노선 추천 모달 [재계산] — 종단 반영 오른쪽 같은 줄 · 늘 켜짐(421) · 보임 = 저장본 없음 · 칸 바꿈 때만(508-2).
-
-./venv/Scripts/python.exe -m pytest resources/tester/test_383_3_rerun_row.py -q
-"""
-
-import re
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-B05 = ROOT / "B05_Profile"
-
-
-def test_재계산_한_줄_늘_켜짐() -> None:
- ts = (B05 / "B05_Profile_UI_RouteEdit_Recommend.ts").read_text(encoding="utf-8")
- assert re.search(r'