feat(B05): 초기노선 갈래 — 제약 못 지킨 구간 한 줄 · 상한 풂 안내 · 기울기 잴 지반 고름 칸

- 비교값 줄 아래 「제약 못 지킨 구간 N곳: No.9 16.4%(지형상 불가능) · …」(없으면 「모두 지킴」 · 5곳 넘으면 「외 N곳」 · 풍선에 전부와 사유)
- relaxed 면 「지형상 14.0% 로 못 이어져 상한을 29.1% 로 풂」(경고 색)
- 공통 제약 줄에 서버 grade_smooth_m 을 「기울기 잴 지반 5m 고름」으로
- 시험 — 글 만들기(test_b05_routeedit_initial) · 사본 왕복에서 목록 · 풂 · 고름 칸이 화면 줄에(test_b05_routeedit_initial_roundtrip)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sCeZz8z8YnbovsKgn7HK7
This commit is contained in:
2026-09-29 10:42:14 +09:00
co-authored by Claude Opus 5.5
parent 258a3512a4
commit 1758ba0fb2
7 changed files with 177 additions and 2 deletions
@@ -79,6 +79,36 @@ const plain = model.normalizeRouteInitial(
over_limit: { grade: 1, radius: 0, stream: 1, total: 2 },
offset_avg_m: 3.25,
offset_max_m: 9.8,
raw_max_grade_pct: 41.2,
violations: [
{
kind: "grade",
from_m: 180,
to_m: 220,
station: "No.9+0.0",
value: 29.13,
limit: 14,
reason: "지형상 불가능 — 복도 안 어떤 길도 23.5% 이상 기울기를 지나야 이어짐",
},
{
kind: "radius",
from_m: 400,
to_m: 412,
station: "No.20+0.0",
value: 9.5,
limit: 12,
reason: "곡선 둘 자리 모자람 — 앞뒤 직선이 짧아 최소 반지름 원호가 안 들어감",
},
{
kind: "stream",
from_m: 600,
to_m: 700,
station: "No.30+0.0",
value: 32,
limit: 100,
reason: "계류 버퍼 안 — 건너기 · 기점 · 종점 봐줌을 넘음",
},
],
},
criteria: {
max_grade_pct: 14,
@@ -86,8 +116,11 @@ const plain = model.normalizeRouteInitial(
stream_offset_m: 5,
road_width_m: 4,
corridor_m: 100,
grade_smooth_m: 5,
pending: ["max_grade_pct", "stream_offset_m"],
},
relaxed: true,
terrain_bound_pct: 23.46,
},
"earthwork",
null,
@@ -132,6 +165,21 @@ const labels = {
criteria: model.criteriaLine(plain.criteria, 0),
criteriaConfirmed: model.criteriaLine({ ...plain.criteria, confirmed: true }, 0),
criteriaFallback: model.criteriaLine(null, 15),
violations: model.violationsLine(plain.metrics),
violationsNone: model.violationsLine({ ...plain.metrics, violations: [] }),
violationsOld: model.violationsLine({ ...plain.metrics, violations: null }),
violationsMany: model.violationsLine({
...plain.metrics,
violations: Array.from({ length: 7 }, (_, i) => ({
kind: "grade",
station: `No.${i}+0.0`,
value: 15 + i,
limit: 14,
reason: "곡선 맞춤 뒤 지반 차 — 측점 사이 지반이 상한을 넘음",
})),
}),
relaxed: model.relaxedLine(plain),
notRelaxed: model.relaxedLine(planShape),
pendingEmpty: model.normalizeRouteInitial(
{
nodes: [
@@ -72,6 +72,9 @@ const out = jobs.map((job) => {
},
label: model.choiceLabel(choice.logic, choice.weights),
metrics: model.metricsLine(choice.metrics),
violations: model.violationsLine(choice.metrics),
relaxed: model.relaxedLine(choice),
criteria: model.criteriaLine(choice.criteria, 0),
};
}
const flat = edits.flattenServerPlan(job.plan.nodes ?? [], job.plan.curves ?? []);
+19 -2
View File
@@ -81,15 +81,32 @@ def test_비교값과_기준값_글(results):
)
assert "로직을 누르면" in labels["metricsNone"]
assert labels["criteria"] == (
"공통 제약 종단기울기 ≤ 14.0% · 최소 R 12m · 계류 이격 ≥ 5.0m (후보 · 사용자 확정 대기)"
"공통 제약 종단기울기 ≤ 14.0% · 최소 R 12m · 계류 이격 ≥ 5.0m · 기울기 잴 지반 5m 고름"
" (후보 · 사용자 확정 대기)"
)
assert labels["criteriaConfirmed"].endswith("5.0m")
assert labels["criteriaConfirmed"].endswith("지반 5m 고름")
assert labels["pendingEmpty"] is True
assert labels["criteriaFallback"] == (
"공통 제약 종단기울기 ≤ — · 최소 R 15m · 계류 이격 ≥ — (후보 · 사용자 확정 대기)"
)
def test_제약_못_지킨_구간과_상한_풂(results):
labels = results["labels"]
assert labels["violations"] == (
"제약 못 지킨 구간 3곳: No.9+0.0 29.1%(지형상 불가능)"
" · No.20+0.0 R 9.5m(곡선 둘 자리 모자람) · No.30+0.0 계류 32.0m(계류 버퍼 안)"
)
assert labels["violationsNone"] == "제약 못 지킨 구간 없음 — 모두 지킴"
assert labels["violationsOld"] == "" # 옛 답(목록 칸 없음)은 줄을 숨긴다
assert labels["violationsMany"].startswith(
"제약 못 지킨 구간 7곳: No.0+0.0 15.0%(곡선 맞춤 뒤 지반 차)"
)
assert labels["violationsMany"].endswith(" 외 2곳")
assert labels["relaxed"] == "지형상 14.0% 로 못 이어져 상한을 23.5% 로 풂"
assert labels["notRelaxed"] == ""
def test_서버가_적어_둔_현재_초기값(results):
bases = results["bases"]
assert bases["follow"] == {"logic": "follow", "weights": None}
@@ -192,6 +192,28 @@ def test_비교값이_응답에_다_있음(rounds):
assert "—" not in item["applied"]["metrics"], item["applied"]["metrics"]
def test_제약_못_지킨_구간이_목록과_화면_줄에(rounds):
for item in rounds["rounds"]:
answer, applied = item["answer"], item["applied"]
violations = answer["metrics"]["violations"]
assert answer["metrics"]["over_limit"]["total"] == len(violations)
for entry in violations:
assert entry["kind"] in ("grade", "radius", "stream")
assert entry["station"].startswith("No.") and entry["reason"]
assert isinstance(entry["value"], (int, float))
head = (
f"제약 못 지킨 구간 {len(violations)}곳: " if violations else "제약 못 지킨 구간 없음"
)
assert applied["violations"].startswith(head), applied["violations"]
if answer.get("relaxed"):
bound = f"{answer['terrain_bound_pct']:.1f}%"
assert applied["relaxed"].endswith(f"상한을 {bound} 로 풂"), applied["relaxed"]
else:
assert applied["relaxed"] == ""
smooth = answer["criteria"]["grade_smooth_m"]
assert f"기울기 잴 지반 {smooth:.0f}m 고름" in applied["criteria"]
def test_확인하면_서버가_초기값을_적고_다시_열면_그대로(rounds, api):
assert len(api.recomputed) == len(LOGICS)
for item in rounds["rounds"]: