Files
Aislo/resources/tester/test_b06_fill_slope_warn.py
eomsangdonandClaude Opus 5 b34eb546f9 feat(b06): 성토사면 길이 5m 초과 경고 줄 — 요약 한 줄 + 측점 목록 · 벽 선 쪽만 뺌(좌·우 갈라) · 경고만(㉳ (나) · 브레인 승인 문구)
- 판정 `_Cross_FillSlope_Warn` — 길이는 카드 머리 「성토사면」 칸과 같은 fillSlopeLengths · 5m 를 넘는 쪽만
- 벽 = 배관 유입(상단측)·유출 기슭막이(집수정은 아님) · 독립 기슭막이 설치 측 · 세월교·BOX암거 양쪽
- 카드 목록 위 <details> — 펼치면 측점 단추, 누르면 그 카드로 · 구조물 자동 배치 없음
- 700줄 — 뷰 조종기 타입을 `_Section_View_Types` 로 뗌(706 → 669)
- 936be972: 37측점 경고 · 5m 넘는 48측점 중 벽 선 11곳 빠짐(80·258.1·260·320·352.1 세월교·440·534·620·720·804.2·982.6)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 00:15:11 +09:00

156 lines
5.7 KiB
Python

"""성토사면 길이 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%) — 설치 여부는 설계자 판단"
)