"""옹벽 물빼기 파이프 단면(73-2) — 횡단도에서 옹벽을 가르는 보기용 형상. 규칙(10-7 사용자 · 브레인): ① 옹벽(`retaining_wall`)만 — 흙막이(찰쌓기 · 콘크리트 등)에는 없음. ② 개수 = 반올림(벽 높이 / 1.5) · 최소 1 — 높이를 그 개수로 고르게 나눈 띠마다 가운데 하나. 1.5 m = 0.75 m 에 1개 · 3 m = 0.75 · 2.25 m 에 2개. ③ 공용 벽 무늬(`appendWallHatch`) 안 — B06 카드 · B07 횡단면도 · 서버 Node 가 한 벌. """ import json import re import subprocess from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] B06 = PROJECT_ROOT / "B06_Section" TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { writeFileSync } from "node:fs"; const make = (tag) => ({ tag, attrs: {}, children: [], text: "", setAttribute(k, v) { this.attrs[k] = String(v); }, removeAttribute(k) { delete this.attrs[k]; }, getAttribute(k) { return this.attrs[k] ?? null; }, append(...nodes) { this.children.push(...nodes); }, set textContent(v) { this.text = v; }, }); globalThis.document = { createElementNS: (_ns, tag) => make(tag) }; const { buildRevetWallGeometry } = await import("./B06_Section_UI_Cross_Wall.js"); const { appendWallHatch } = await import("./B06_Section_UI_Cross_Wall_Hatch.js"); const walk = (node, out = []) => { if (node.tag === "g" && node.attrs.class === "b06-chart__wall-drain") out.push(node); for (const child of node.children ?? []) walk(child, out); return out; }; const drains = (heightM, form, retaining, outward = 1) => { const wall = buildRevetWallGeometry({ anchor: { offset: 5, elevation: 100 }, outward, height: heightM - 0.5, material: "concrete", role: "outlet", form, }); if (retaining) wall.retaining = true; const layer = make("svg"); appendWallHatch(layer, wall, (o) => o, (e) => -e, "t"); const bottom = wall.bottomBack.elevation; const xs = wall.points.map((p) => p.offset); const [lo, hi] = [Math.min(...xs) - 1e-6, Math.max(...xs) + 1e-6]; return walk(layer).map((pipe) => { const lines = pipe.children.filter((c) => c.tag === "line"); const mid = lines.map((l) => -(Number(l.attrs.y1) + Number(l.attrs.y2)) / 2); const drop = lines.map((l) => Number(l.attrs.y2) - Number(l.attrs.y1)); // x1 = 배면 return { lines: lines.length, center: (mid[0] + mid[1]) / 2 - bottom, frontLower: drop.every((d) => d > 0), inside: lines.every((l) => [l.attrs.x1, l.attrs.x2].every((v) => lo <= +v && +v <= hi)), title: pipe.children.find((c) => c.tag === "title")?.text, cls: lines.map((l) => l.attrs.class ?? null), }; }); }; writeFileSync(process.argv[2], JSON.stringify({ h15: drains(1.5, "콘크리트", true), h30: drains(3.0, "콘크리트", true), h30right: drains(3.0, "중력식", true, -1), low: drains(0.6, "콘크리트", true), wet: drains(3.0, "돌쌓기(찰)", false), concrete: drains(3.0, "콘크리트", false), })); """ @pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None: out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(B06 / "B06_Section_UI_Cross_Wall.ts"), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--skipLibCheck", "--ignoreConfig", ], cwd=str(PROJECT_ROOT), capture_output=True, ) for emitted in out.rglob("*.js"): emitted.write_text( re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")), encoding="utf-8", ) base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Wall.js")) (base / "runner.mjs").write_text(_RUNNER, encoding="utf-8") result = tmp_path / "out.json" subprocess.run( # noqa: S603 ["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True ) got = json.loads(result.read_text(encoding="utf-8")) assert [p["center"] for p in got["h15"]] == pytest.approx([0.75], abs=0.01) assert [p["center"] for p in got["h30"]] == pytest.approx([0.75, 2.25], abs=0.01) assert [p["center"] for p in got["h30right"]] == pytest.approx([0.75, 2.25], abs=0.01) assert len(got["low"]) == 1, "낮은 옹벽도 최소 1개" assert got["wet"] == [] and got["concrete"] == [], "흙막이(찰쌓기 · 콘크리트)에 파이프가 섬" for pipe in got["h15"] + got["h30"] + got["h30right"]: assert pipe["lines"] == 2, "두 줄 평행선이 아님" assert pipe["frontLower"], "앞쪽이 낮지 않음" assert pipe["inside"], "파이프가 벽 밖으로 나감(B07 수확은 clip 이 없음)" assert pipe["title"] == "물빼기(보기용)" assert pipe["cls"] == [None, None], "선 색은 묶음 토큰이 정함" def test_옹벽_표식은_구조물_종류로() -> None: source = (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(encoding="utf-8") assert 'spec.type_id === "retaining_wall"' in source css = (B06 / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8") assert re.search(r"\.b06-chart__wall-drain line \{[^}]*var\(--color-", css), "색 토큰 없음"