fix(B06): 양측 성토 측점 측구 판정 = 판정 역할 기준 · 옛 저장분 다시 셈 · 그려진 측구 단추 보임(206-1)
- 원인: 측구 판정의 양측 성토 조건이 입력 단면유형(옛 저장 right_cut)을 봄 · 계획고 변경으로 지형 판정은 양측 성토인데 옛 결과 ditch_enabled 가 선택으로 둔갑해 측구가 섬 - 엔진(파이썬 · TS 짝): 판정 역할(양쪽 성토)로 측구 없음 · 남은 선택 비움 - 낡음 판정: 양측 성토 + 측구 결과 저장분 → B05 · B06 진입 때 다시 셈 - 179-2 숨김: 측구가 그려져 있으면 측구 · 형식 단추 보임 - 시험: test_206_1(검증_sub1 No.1+0 사본 재현 · 거울) · 거울 시험 ditch_choice 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -121,7 +121,11 @@ class _SectionGeometry:
|
||||
# ⚠ 옛 저장분(`ditch_choice` 가 없던 것)은 `legacy_ditch_enabled` 로 온다. 그 값은
|
||||
# **자동값과 다를 때만 뜻이 있다** — 같으면 자동이 그렇게 냈던 것이고, 다르면
|
||||
# 사용자가 일부러 바꾼 것이다. 그래서 **다를 때만** 선택으로 살린다(설계 의도 보존).
|
||||
if section_mode == "both_fill":
|
||||
# ⚠ 양측 성토는 **판정된 역할**로 가른다(206-1) — 입력 `section_mode` 는 옛 저장값일 수
|
||||
# 있다(계획고를 올려 우 절토 → 양측 성토 · 입력은 right_cut 그대로). 입력으로 가르면 옛
|
||||
# 측구 결과가 선택으로 둔갑해 양측 성토 단면에 측구가 섰다. ⚠ TS 짝 `SectionGeometry`.
|
||||
both_fill = self.left_role == "fill" and self.right_role == "fill"
|
||||
if both_fill:
|
||||
auto_ditch = False
|
||||
elif ground_at is not None:
|
||||
ditch_edge = self.left_extent if ditch_side == "left" else -self.right_extent
|
||||
@@ -136,10 +140,10 @@ class _SectionGeometry:
|
||||
# 없는데, 그것을 선택으로 굳히면 **다시 같은 병**(지형이 바뀌어도 안 따라감)이 된다.
|
||||
if choice is not None and bool(choice) == auto_ditch:
|
||||
choice = None
|
||||
# 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다.
|
||||
self.has_ditch = auto_ditch if (choice is None or section_mode == "both_fill") else choice
|
||||
# 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저 · 남은 선택은 비움.
|
||||
self.has_ditch = auto_ditch if (choice is None or both_fill) else choice
|
||||
#: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다.
|
||||
self.ditch_choice = choice
|
||||
self.ditch_choice = None if both_fill else choice
|
||||
|
||||
# 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
|
||||
# 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도.
|
||||
|
||||
@@ -388,8 +388,10 @@ export function buildDesignControls(
|
||||
groundSegment.classList.add("b06-design__seg--header");
|
||||
// 단면 유형은 지형에서 자동 판정되며(D-2), 제목행 pill로 표시한다(E-3, Cross_View에서 생성).
|
||||
// 아래 옵션은 선행 조건 미충족 시 비활성 = 숨김(179-2 · CSS) — 편측의 쪽은 알약 글에.
|
||||
const rockCut = isRock(state.ground) && state.mode !== "both_fill";
|
||||
const hasDitch = state.mode !== "both_fill";
|
||||
// 숨김은 「막힘 + 그려진 측구 없음」일 때만(206-1) — 측구가 그려져 있으면 단추를 보여 끌 수 있게.
|
||||
const ditchDrawn = design?.ditch_enabled === true;
|
||||
const rockCut = isRock(state.ground) && (state.mode !== "both_fill" || ditchDrawn);
|
||||
const hasDitch = state.mode !== "both_fill" || ditchDrawn;
|
||||
// 측구 생성 여부(자동 판정 or 사용자 override) — 측구형식은 측구가 있을 때만 의미 있다.
|
||||
// 화면 표시는 **결과**를 보인다 — 선택이 없으면 자동으로 선 결과가 답이다.
|
||||
const ditchOn = state.ditchEnabled ?? design?.ditch_enabled ?? true;
|
||||
|
||||
@@ -106,6 +106,8 @@ interface StaleCandidate {
|
||||
geometry_preset?: string;
|
||||
two_stage_slope?: boolean;
|
||||
surface_drop_m?: number;
|
||||
section_mode?: string;
|
||||
ditch_enabled?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -120,6 +122,8 @@ function isStaleSection(
|
||||
// 이 조건이 B06 페이지에만 있어 B05는 재계산을 건너뛰었고, 같은 데이터인데 두 화면의
|
||||
// 절토량이 갈렸다(2026-09-03 실측 228.52㎡ ↔ 270.12㎡). 판정을 여기 한 곳으로 모은다.
|
||||
if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true;
|
||||
// 옛 엔진이 양측 성토에 측구를 세워 둔 저장분(206-1) — 지금 엔진으로 다시 셈.
|
||||
if (design.section_mode === "both_fill" && design.ditch_enabled === true) return true;
|
||||
if (!profiles?.length) return false;
|
||||
const planned = designElevationAt(profiles, section.chainage_m);
|
||||
if (planned === undefined) return false;
|
||||
|
||||
@@ -188,8 +188,11 @@ export class SectionGeometry {
|
||||
// `hasDitch`** 로 가른다.
|
||||
// ⚠ 옛 저장분은 `ditchEnabled` 로 온다. 그 값은 **자동값과 다를 때만 뜻이 있다** —
|
||||
// 같으면 자동이 그렇게 냈던 것이고, 다르면 사용자가 일부러 바꾼 것이다.
|
||||
// ⚠ 양측 성토는 **판정된 역할**로 가른다(206-1 · 짝: 파이썬) — 입력 `sectionMode` 는 옛
|
||||
// 저장값일 수 있다. 입력으로 가르면 옛 측구 결과가 선택으로 둔갑해 양측 성토에 측구가 섰다.
|
||||
const bothFill = this.leftRole === "fill" && this.rightRole === "fill";
|
||||
let autoDitch: boolean;
|
||||
if (params.sectionMode === "both_fill") {
|
||||
if (bothFill) {
|
||||
autoDitch = false;
|
||||
} else if (groundAt !== null) {
|
||||
const ditchEdge = params.ditchSide === "left" ? this.leftExtent : -this.rightExtent;
|
||||
@@ -206,10 +209,10 @@ export class SectionGeometry {
|
||||
// 「자동으로 되돌리기」 단추가 없고, 원래 값으로 다시 누른 것을 선택으로 굳히면
|
||||
// **다시 같은 병**이 된다.
|
||||
if (choice !== null && Boolean(choice) === autoDitch) choice = null;
|
||||
// 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저다.
|
||||
this.hasDitch = choice === null || params.sectionMode === "both_fill" ? autoDitch : choice;
|
||||
// 양성(both_fill)은 측구가 설 자리가 없다 — 선택보다 기하가 먼저 · 남은 선택은 비움.
|
||||
this.hasDitch = choice === null || bothFill ? autoDitch : choice;
|
||||
/** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */
|
||||
this.ditchChoice = choice;
|
||||
this.ditchChoice = bothFill ? null : choice;
|
||||
|
||||
// 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전**
|
||||
// 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`.
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 206-1 — 양측 성토 측점에 측구가 남는 흠(검증_sub1 No.1+0 사본 재현).
|
||||
|
||||
원인 — 측구 판정의 「양측 성토 = 측구 없음」이 **입력** `section_mode` 를 봤다. 입력은 옛 저장값
|
||||
(초기값 right_cut)이고, 계획고를 +1.98 m 올려 지형 판정(역할)은 양측 성토가 됐는데 입력이 그대로라
|
||||
옛 결과 `ditch_enabled=True` 가 사용자 선택으로 둔갑(ditch_choice=True)해 측구가 섰다.
|
||||
좌 · 우를 누르면 카드가 판정값 both_fill 을 입력으로 다시 보내 그때서야 사라졌다.
|
||||
고침 — 판정된 역할(양쪽 성토)로 가름 · 파이썬 · TS 짝 같은 규칙 · 옛 저장분은 낡음으로 다시 셈.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
|
||||
# 검증_sub1(7e420ed6) 노선 230 · No.1+0(20 m) 지반선 사본 — -20 ~ +20 m · 0.5 m 간격
|
||||
_Z = [
|
||||
885.0166, 884.8327, 884.6401, 884.4294, 884.2270, 883.7564, 883.2482, 882.8840, 882.5737,
|
||||
882.3786, 882.2795, 881.9692, 881.3788, 880.7761, 880.1219, 879.4944, 879.1738, 878.8550,
|
||||
878.6597, 878.4670, 878.1648, 877.8416, 877.5119, 877.1749, 876.8564, 876.5584, 876.2612,
|
||||
875.9646, 875.6791, 875.4169, 875.1405, 874.7286, 874.3079, 874.0026, 873.7065, 873.4074,
|
||||
873.1107, 872.7726, 872.4127, 872.1306, 871.9252, 871.7424, 871.6078, 871.3723, 870.7716,
|
||||
870.1964, 869.8586, 869.5135, 869.2563, 869.0105, 868.5769, 868.0912, 867.7274, 867.4369,
|
||||
867.1615, 866.9033, 866.6590, 866.4489, 866.2281, 865.9711, 865.7139, 865.4391, 865.1650,
|
||||
864.8953, 864.6289, 864.3219, 863.9996, 863.6703, 863.3343, 863.0443, 862.8261, 862.5776,
|
||||
862.2364, 861.9035, 861.6491, 861.3932, 861.1137, 860.8302, 860.6107, 860.4051, 860.2006,
|
||||
] # fmt: skip
|
||||
SAMPLES = [{"offset_m": -20.0 + 0.5 * i, "elevation_m": z} for i, z in enumerate(_Z)]
|
||||
ELEV = 873.900731 # 지금 계획고(초기값 871.9252 에서 +1.98 m)
|
||||
# 그 프로젝트 저장분과 같은 입력 — 옛 단면유형 · 옛 측구 결과
|
||||
STORED = {
|
||||
"ground_type": "ripping_rock",
|
||||
"ditch_side": "right",
|
||||
"ditch_type": "standard",
|
||||
"rock_boundary_offset_m": -0.5,
|
||||
"two_stage_slope": True,
|
||||
}
|
||||
|
||||
|
||||
_GROUP = {
|
||||
"road_width_m": 3,
|
||||
"shoulder_left_m": 0.5,
|
||||
"shoulder_right_m": 0.5,
|
||||
"ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3},
|
||||
"fill_slope_ratio": 1.2,
|
||||
}
|
||||
# 그 프로젝트 종단 options 의 표준 횡단면
|
||||
STANDARD = {
|
||||
"soil": {**_GROUP, "cross_slope_pct": {"min": 3, "max": 5}, "cut_slope_ratio": 1},
|
||||
"rock": {
|
||||
**_GROUP,
|
||||
"ditch_l_type": {"width_m": 0.5, "depth_m": 0.1},
|
||||
"cross_slope_pct": {"min": 3, "max": 5},
|
||||
"cut_slope_ratio": 0.4,
|
||||
},
|
||||
"paved": {
|
||||
**_GROUP,
|
||||
"cross_slope_pct": {"min": 1.5, "max": 2},
|
||||
"cut_slope_ratio": 1,
|
||||
"pavement_thickness_m": 0.2,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _design(section_mode: str, **kwargs):
|
||||
return compute_cross_design(
|
||||
SAMPLES, ELEV, section_mode=section_mode, standard=STANDARD, **STORED, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stale",
|
||||
[
|
||||
{"ditch_enabled": True}, # 옛 결과만(초기값 → 계획고 변경 재계산)
|
||||
{"ditch_enabled": True, "ditch_choice": True}, # 둔갑한 선택이 저장된 뒤(지금 DB)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("mode", ["right_cut", "both_cut", "left_cut"])
|
||||
def test_사본_양측_성토면_옛_입력이어도_측구_없음(mode: str, stale: dict) -> None:
|
||||
design = _design(mode, **stale)
|
||||
assert design["section_mode"] == "both_fill"
|
||||
assert design["ditch_enabled"] is False, "양측 성토인데 측구가 섰다"
|
||||
assert design["ditch_area_m2"] == 0.0
|
||||
assert design["ditch"] == {"type": "none"}
|
||||
assert design["ditch_choice"] is None, "옛 결과가 선택으로 남았다"
|
||||
|
||||
|
||||
def test_사본_옛_입력과_바른_입력이_같은_형상() -> None:
|
||||
"""좌 · 우를 눌러야만 사라지던 것 — 이제 입력이 옛 값이어도 처음부터 같은 형상 · 면적."""
|
||||
stale = _design("right_cut", ditch_enabled=True, ditch_choice=True)
|
||||
fresh = _design("both_fill")
|
||||
for key in ("design_line", "fill_area_m2", "cut_area_m2", "ditch_area_m2"):
|
||||
assert stale[key] == fresh[key], key
|
||||
assert fresh["fill_area_m2"] == pytest.approx(34.0513, abs=1e-4)
|
||||
# 우 노견 바깥에 측구 꼭짓점(-2.3 · -2.6 · -2.9 m = 상단 0.9 · 저폭 0.3 사다리꼴)이 없다
|
||||
offsets = {p["offset_m"] for p in fresh["design_line"]}
|
||||
assert not offsets & {-2.3, -2.6, -2.9}
|
||||
|
||||
|
||||
def test_한쪽이라도_절토면_측구_그대로() -> None:
|
||||
"""양측 성토 판정은 역할로만 — 우 절토 지형은 옛 입력 both_fill 이어도 측구 섬."""
|
||||
design = _design("both_fill", ditch_enabled=False)
|
||||
assert design["section_mode"] == "both_fill" # 이 지형은 양측 성토
|
||||
cut = compute_cross_design(SAMPLES, ELEV - 2.5, section_mode="both_fill", **STORED)
|
||||
assert cut["section_mode"] in ("right_cut", "both_cut")
|
||||
assert cut["ditch_enabled"] is True and cut["ditch_side"] == "right"
|
||||
|
||||
|
||||
def test_TS_짝도_같은_값(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""거울 — 같은 사본을 TS `computeCrossDesign` 에 넣어 파이썬과 같은지(측구 없음까지)."""
|
||||
import test_b06_cross_design_mirror as mirror
|
||||
|
||||
cases = [
|
||||
{
|
||||
"name": f"206-1 사본 {mode}",
|
||||
"samples": SAMPLES,
|
||||
"design_elevation_m": ELEV,
|
||||
"options": {
|
||||
"section_mode": mode,
|
||||
**STORED,
|
||||
"ditch_enabled": True,
|
||||
"ditch_choice": True,
|
||||
},
|
||||
}
|
||||
for mode in ("right_cut", "both_fill")
|
||||
]
|
||||
monkeypatch.setattr(mirror, "CASES", cases)
|
||||
py, ts = mirror._python_results(), mirror._ts_results(tmp_path)
|
||||
problems = []
|
||||
for case, left, right in zip(cases, py, ts):
|
||||
problems.extend(mirror._diff(left, right, case["name"]))
|
||||
assert not problems, "\n".join(problems[:20])
|
||||
assert all(item["ditch_enabled"] is False for item in ts)
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
return (PROJECT_ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_옛_저장분은_낡음으로_다시_셈() -> None:
|
||||
"""DB 에 남은 양측 성토 + 측구 결과 → B05 · B06 진입 때 브라우저가 다시 셈(공용 낡음 판정)."""
|
||||
common = _src("B06_Section/B06_Section_UI_Section_Common.ts")
|
||||
assert 'design.section_mode === "both_fill" && design.ditch_enabled === true' in common
|
||||
|
||||
|
||||
def test_측구가_그려져_있으면_단추를_숨기지_않음() -> None:
|
||||
"""179-2 숨김은 「막힘 + 그려진 측구 없음」일 때만 — 그려진 측구는 끌 수 있어야 함."""
|
||||
cd = _src("B06_Section/B06_Section_UI_Cross_Design.ts")
|
||||
assert "const ditchDrawn = design?.ditch_enabled === true;" in cd
|
||||
assert 'const hasDitch = state.mode !== "both_fill" || ditchDrawn;' in cd
|
||||
assert '(state.mode !== "both_fill" || ditchDrawn)' in cd # 측구형식도 같은 규칙
|
||||
@@ -234,6 +234,7 @@ const camel = {
|
||||
two_stage_slope: "twoStageSlope",
|
||||
cut_slope_ratio: "cutSlopeRatio",
|
||||
ditch_enabled: "ditchEnabled",
|
||||
ditch_choice: "ditchChoice",
|
||||
surface_drop_m: "surfaceDropM",
|
||||
plan_radius_m: "planRadiusM",
|
||||
curve_outer_side: "curveOuterSide",
|
||||
|
||||
Reference in New Issue
Block a user