feat(b06): 사면길이 3m 초과 소단 검토 정보 한 줄 — 성토사면 경고 밑 흐린 색 · 절·성토 둘 다 소단 사이 가장 긴 도막 · 벽 선 쪽 뺌 · 경고 아님(별표2 차.(5) 「붕괴 우려 지역」은 우리가 판정 못 함 · 브레인 ③)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-15 06:27:28 +09:00
co-authored by Claude Opus 5
parent 35c0a8203a
commit b1bdcb643d
5 changed files with 267 additions and 12 deletions
@@ -0,0 +1,108 @@
"""사면길이 3m 초과 — 소단 **검토 대상 정보 한 줄**(경고 아님 · 2026-09-15 브레인 ③).
① 법령 별표2 Ⅰ.2.차.(5) 「붕괴 또는 밀려 내려갈 **우려가 있는 지역**에는 사면길이 2~3m 마다」 —
「우려 있음」을 우리가 판정 못 함 ⇒ 경고가 아니라 정보
② 3m 를 **넘는** 쪽만(3.00 은 아님) · 절토·성토 둘 다 · 원지반을 못 만난 하한값(≥)도 넘으면 들어감
③ 벽이 선 쪽은 뺌(성토사면 5m 경고와 같은 규칙 · `wallSides`)
④ 문구는 브레인 문구 그대로
TS 를 실제로 돌린다(파이썬 짝이 없는 화면 판정 — `test_b06_fill_slope_warn` 과 같은 방식).
"""
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"
FOLDER = PROJECT_ROOT / "B06_Section"
SOURCES = [
FOLDER / "B06_Section_UI_Cross_Berm_Info.ts",
FOLDER / "B06_Section_UI_Cross_FillSlope_Warn.ts",
FOLDER / "B06_Section_UI_Cross_Culvert_Const.ts",
]
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import { BERM_INFO_TEXT, bermReviewStations } from "./B06_Section_UI_Cross_Berm_Info.js";
const [inputPath, outputPath] = process.argv.slice(2);
const cases = JSON.parse(readFileSync(inputPath, "utf8"));
const rows = bermReviewStations(cases.map((c) => c.section), (section) =>
cases.find((c) => c.section.station_id === section.station_id).lengths,
);
writeFileSync(outputPath, JSON.stringify({
text: BERM_INFO_TEXT,
rows: rows.map((w) => [w.section.station_id, w.sides.map((s) => s.side)]),
}));
"""
LONG = {"lengthM": 4.0, "open": False}
CASES = [
{"section": {"station_id": "both"}, "lengths": {"left": LONG, "right": LONG}},
{
"section": {"station_id": "edge"},
"lengths": {
"left": {"lengthM": 3.0, "open": False},
"right": {"lengthM": 2.5, "open": True},
},
},
{
"section": {"station_id": "open_long"},
"lengths": {"left": None, "right": {"lengthM": 3.4, "open": True}},
},
{"section": {"station_id": "ford", "ford": {}}, "lengths": {"left": LONG, "right": LONG}},
]
def _run(tmp_path: Path) -> dict:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
*map(str, SOURCES),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
"--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_3m_넘는_사면만_벽_선_쪽은_뺀다(tmp_path: Path) -> None:
got = _run(tmp_path)
assert dict(got["rows"]) == {"both": ["left", "right"], "open_long": ["right"]}
assert got["text"] == "사면길이 3m 초과 — 소단 검토 대상(별표2 차.(5) · 붕괴 우려 지역 조건)"