"""터파기 단면 — 파이썬·TS 짝이 같은 값을 내는지 (2026-09-09). 화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽이 갈리면 「그림은 이런데 수량은 저렇다」가 된다. ⚠ 근거의 급이 다른 두 갈래를 함께 잠근다 — 관은 **법정 표**(KCS 44 40 10 그림 3.2-1), 벽은 **실무 정본 식**(구조도 기슭막이 xls). """ from __future__ import annotations import json import re import subprocess import sys from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from common_util.common_util_excavation import ( # noqa: E402 PIPE_TRENCH_WIDTH_MM, WALL_FOUNDATION_DEPTH_M, WALL_FOUNDATION_WIDTH_M, WALL_TRENCH_CLEARANCE_M, pipe_trench_width_m, wall_trench_area_m2, wall_trench_width_m, ) TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" _RUNNER = """ import { writeFileSync } from "node:fs"; import { PIPE_TRENCH_WIDTH_MM, pipeTrenchWidthM, wallTrenchAreaM2, wallTrenchWidthM, } from "./common_util_excavation.js"; const out = { table: PIPE_TRENCH_WIDTH_MM, pipe: [300, 800, 1000, 1200, 1500, 250, 1600].map((d) => pipeTrenchWidthM(d)), wallWithFoundation: [1.0, 2.0, 3.0].map((h) => wallTrenchAreaM2(h, 0.7, true)), wallBlindingOnly: [1.0, 2.0, 3.0].map((h) => wallTrenchAreaM2(h, 0.7, false)), wallWidth: [0.7, 1.05, 0].map((t) => wallTrenchWidthM(t)), nulls: [pipeTrenchWidthM(null), wallTrenchAreaM2(null, 0.7, true), wallTrenchAreaM2(1, null, true)], }; writeFileSync(process.argv[2], JSON.stringify(out)); """ def _ts_values(tmp_path: Path) -> dict: out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(PROJECT_ROOT / "common_util" / "common_util_excavation.ts"), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", ], cwd=str(PROJECT_ROOT), check=True, capture_output=True, ) for emitted in out.glob("*.js"): text = emitted.read_text(encoding="utf-8") emitted.write_text( re.sub(r'(from "\./[^"]+?)(")', lambda m: m.group(1) + ".js" + m.group(2), text), encoding="utf-8", ) (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") result = tmp_path / "out.json" subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(result)], cwd=str(PROJECT_ROOT), check=True, capture_output=True, ) return json.loads(result.read_text(encoding="utf-8")) @pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") def test_두_쪽이_같은_값을_낸다(tmp_path: Path) -> None: ts = _ts_values(tmp_path) assert {int(k): v for k, v in ts["table"].items()} == PIPE_TRENCH_WIDTH_MM assert ts["pipe"] == [pipe_trench_width_m(d) for d in (300, 800, 1000, 1200, 1500, 250, 1600)] assert ts["wallWithFoundation"] == [ wall_trench_area_m2(h, 0.7, has_foundation=True) for h in (1.0, 2.0, 3.0) ] assert ts["wallBlindingOnly"] == [ wall_trench_area_m2(h, 0.7, has_foundation=False) for h in (1.0, 2.0, 3.0) ] assert ts["wallWidth"] == [wall_trench_width_m(t) for t in (0.7, 1.05, 0)] assert ts["nulls"] == [None, None, None] def test_관은_표를_그대로_쓴다() -> None: """⚠ 관경 + 2b 로 계산하지 않는다 — 표가 관 두께·작업여유를 담고 있다.""" assert pipe_trench_width_m(800) == 1.6 assert pipe_trench_width_m(800) != 0.8 + 2 * 0.3 for diameter in (800, 1000, 1200, 1500): assert pipe_trench_width_m(diameter) is not None, diameter def test_표에_없는_관경은_지어내지_않는다() -> None: assert pipe_trench_width_m(250) is None assert pipe_trench_width_m(1600) is None def test_실무_정본_수치와_맞는다() -> None: """xls 7탭: H=1.0 → 1.03+0.45 = 1.48 · H=2.0 → 2.51 · H=3.0 → 3.54 (평균두께 0.83).""" thickness = 0.83 for height, expected in ((1.0, 1.48), (2.0, 2.51), (3.0, 3.54)): area = wall_trench_area_m2(height, thickness, has_foundation=True) assert area == pytest.approx(expected, abs=0.01), height def test_기초_유무가_기초분을_가른다() -> None: with_base = wall_trench_area_m2(2.0, 0.7, has_foundation=True) without = wall_trench_area_m2(2.0, 0.7, has_foundation=False) assert with_base - without == pytest.approx( WALL_FOUNDATION_DEPTH_M * WALL_FOUNDATION_WIDTH_M - 0.1 * 0.7 ) def test_여유폭은_평균두께에_더한다() -> None: assert wall_trench_width_m(0.7) == pytest.approx(0.7 + WALL_TRENCH_CLEARANCE_M)