"""성토사면 길이 5m 초과 경고 — 벽 선 쪽만 빼고(좌·우 갈라) 경고만 (2026-09-14 브레인 (나)). ① 5m 를 **넘는** 쪽만 — 5.00 은 아님 · 원지반을 못 만난 하한값(≥)도 5m 이하면 아님 ② 벽이 선 쪽은 뺀다 — 배관 유입(상단측)·유출(반대측) 기슭막이, 집수정은 벽 아님 ③ 한쪽에만 벽 → 반대쪽은 그대로 경고 · 독립 기슭막이 설치 측(좌/우/양쪽) ④ 세월교·BOX암거는 양쪽 측벽 ⑤ 문구는 브레인 승인 그대로 TS 를 실제로 돌린다(파이썬 짝이 없는 화면 판정). """ from __future__ import annotations import json import re import subprocess from pathlib import Path import pytest PROJECT_ROOT = Path(__file__).resolve().parents[2] TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" SOURCE = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Cross_FillSlope_Warn.ts" _RUNNER = """ import { readFileSync, writeFileSync } from "node:fs"; import { FILL_SLOPE_WARN_TEXT, fillSlopeWarnings } from "./B06_Section_UI_Cross_FillSlope_Warn.js"; const [inputPath, outputPath] = process.argv.slice(2); const cases = JSON.parse(readFileSync(inputPath, "utf8")); const warnings = fillSlopeWarnings(cases.map((c) => c.section), (section) => cases.find((c) => c.section.station_id === section.station_id).lengths, ); writeFileSync(outputPath, JSON.stringify({ text: FILL_SLOPE_WARN_TEXT, warnings: warnings.map((w) => [w.section.station_id, w.sides.map((s) => s.side)]), })); """ LONG = {"lengthM": 7.0, "open": False} BOTH = {"left": LONG, "right": LONG} def _culvert(inlet: str = "기슭막이", outlet: str = "기슭막이", **extra: object) -> dict: return {"inlet": {"structure": inlet}, "outlet": {"structure": outlet}, **extra} CASES = [ {"section": {"station_id": "plain"}, "lengths": BOTH}, { "section": {"station_id": "edge"}, "lengths": { "left": {"lengthM": 5.0, "open": False}, "right": {"lengthM": 4.2, "open": True}, }, }, # 상단측 좌 → 유입(좌) 기슭막이 · 유출(우) 기슭막이 — 양쪽 다 벽. { "section": {"station_id": "pipe", "uphill_side": "left", "culvert": _culvert()}, "lengths": BOTH, }, # 유입이 집수정 → 좌(유입측)는 벽 없음 → 좌만 경고. { "section": {"station_id": "basin", "uphill_side": "left", "culvert": _culvert("집수정")}, "lengths": BOTH, }, # 상단측 우 → 유출은 좌 · 유출이 집수정이면 좌만 경고. { "section": { "station_id": "right_up", "uphill_side": "right", "culvert": _culvert(outlet="집수정"), }, "lengths": BOTH, }, { "section": {"station_id": "own_left", "culvert": _culvert(hidden_pipe=True, side="좌")}, "lengths": BOTH, }, { "section": { "station_id": "revet_auto", "revetment": {"side": None}, "design": {"section_mode": "left_cut"}, }, "lengths": {"left": None, "right": LONG}, }, {"section": {"station_id": "ford", "ford": {}}, "lengths": BOTH}, ] def _run(tmp_path: Path) -> dict: out = tmp_path / "js" subprocess.run( # noqa: S603 — 고정 실행 파일 [ "node", str(TSC), str(SOURCE), # 실행에 드는 import 는 이것 하나 — 나머지는 타입 import 라 지워진다. str(SOURCE.with_name("B06_Section_UI_Cross_Culvert_Const.ts")), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", # 타입 줄기가 별칭(@util 등)으로 번져 단독 컴파일로는 못 푼다 — 검사는 typecheck 몫. "--noCheck", "--noResolve", ], 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") payload, result = tmp_path / "input.json", tmp_path / "output.json" payload.write_text(json.dumps(CASES, ensure_ascii=False), encoding="utf-8") subprocess.run( # noqa: S603 ["node", str(out / "runner.mjs"), str(payload), 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_벽_선_쪽만_빼고_5m_넘는_쪽을_경고한다(tmp_path: Path) -> None: got = dict(_run(tmp_path)["warnings"]) assert got == { "plain": ["left", "right"], "basin": ["left"], "right_up": ["left"], "own_left": ["right"], } # 5.00 · 하한값 4.2(≥) · 양쪽 벽 · 자동 설치 측 벽 · 세월교는 경고 없음. assert not {"edge", "pipe", "revet_auto", "ford"} & set(got) @pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") def test_문구는_승인된_그대로(tmp_path: Path) -> None: assert _run(tmp_path)["text"] == ( "성토사면 길이 5m 초과 — 법령상 옹벽·석축 설치 대상 " "(산림자원법 시행규칙 별표2 Ⅰ.2.차.(3).(나) · 임도설치 규정 별표7 2.차.(3).(나)) " "※ 실무 표본에서도 흔함(영월 63% · 봉화 49%) — 설치 여부는 설계자 판단" )