- 판정 `_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
156 lines
5.7 KiB
Python
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%) — 설치 여부는 설계자 판단"
|
|
)
|