diff --git a/B06_Section/B06_Section_Router_Design.py b/B06_Section/B06_Section_Router_Design.py index 05ef8acb7..d1fffa572 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -235,6 +235,58 @@ def enforce_pavement_ranges( return changed +def enforce_design_lines( + longitudinal: dict[str, Any], + cross_sections: list[dict[str, Any]], + project_root: Path, + standard: dict[str, Any] | None = None, +) -> int: + """[저장]·[확정] 때 **모든 측점** 설계선을 저장된 입력으로 서버가 다시 낸다(139-6). + + 그전에는 포장 · 세월교 · 사토장 측점만 다시 냈고 보통 측점은 브라우저가 보낸 설계를 그대로 + 받아 적었다(CLAUDE.md 5장 「[저장]은 서버가 정본으로 다시 계산」 어긋남 · B07 이 저장된 선을 + 그림). 입력 = 저장분(지반유형 · 단면유형 · 측구 · 포장 · 암 경계 · 암 절토 경사 · 소단 · 노견 + 확장) + 지금 종단 계획고 · 세월교 월류 하강 · 곡선부 확폭. 사용자 조작값은 승계. + """ + ford_drops = ford_surface_drops(project_root) + changed = 0 + for section in cross_sections: + design = section.get("design") + if not isinstance(design, dict): + continue + chainage = float(section.get("chainage_m", 0.0)) + try: + recomputed = compute_cross_design( + section.get("samples", []), + design_elevation_from_longitudinal(longitudinal, chainage), + ground_type=str(design.get("ground_type") or "ripping_rock"), + section_mode=str(design.get("section_mode") or "left_cut"), + ditch_side=design.get("ditch_side"), + ditch_type=str(design.get("ditch_type") or "standard"), + paved=bool(design.get("paved", False)), + standard=standard, + rock_boundary_offset_m=design.get( + "rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M + ), + two_stage_slope=bool(design.get("two_stage_slope", True)), + cut_slope_ratio=stored_cut_slope(design), + ditch_enabled=design.get("ditch_enabled"), + ditch_choice=design.get("ditch_choice"), + surface_drop_m=ford_drop_at(chainage, ford_drops), + berm=stored_berm(design), + **curve_widening_args(section), + **stored_shoulder_extend(design), + ) + except (ValueError, KeyError): + continue + for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS): + if design.get(key) is not None: + recomputed[key] = design[key] + section["design"] = recomputed + changed += 1 + return changed + + def enforce_ford_surface_drops( longitudinal: dict[str, Any], cross_sections: list[dict[str, Any]], diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 875f81b34..2a965c06d 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -241,6 +241,13 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: standard = stored_standard_cross_section(longitudinal_row) # 포장 구간·세월교 보정 — 고쳐진 설계 위에서 면적·유토곡선이 나와야 한다. before = [json.dumps(item.get("design"), sort_keys=True, default=str) for item in sections] + # 모든 측점 설계선을 저장 입력으로 먼저 다시 냄(139-6 · 노견 확장 · 측구 · 지반유형) — 미교차 + # 판정도 새 설계로 해야 넓힘 지표면을 연다. + from B06_Section.B06_Section_Router_Design import enforce_design_lines + + await asyncio.to_thread( + enforce_design_lines, detail.get("longitudinal") or {}, sections, project_root, standard + ) # 확정 지표면 — 미교차 측점이 있을 때만 연다(샘플 넓힘 ㉳ (가) · 없으면 여는 비용을 안 씀). unclosed = any((item.get("design") or {}).get("slope_unclosed") for item in sections) sampler = await asyncio.to_thread(_surface_sampler, project_root, surface) if unclosed else None diff --git a/resources/tester/test_139_6_save_redesign.py b/resources/tester/test_139_6_save_redesign.py new file mode 100644 index 000000000..bf884c31f --- /dev/null +++ b/resources/tester/test_139_6_save_redesign.py @@ -0,0 +1,72 @@ +"""139-6 [저장] 때 서버가 모든 측점 설계선을 저장 입력으로 다시 냄 — 노견 확장이 설계선에 들어감. + +그전에는 포장 · 세월교 · 사토장 측점만 다시 냈고 보통 측점은 브라우저 설계를 받아 적음 +(`B07` 이 저장된 선을 그려 쉬프트가 안 보였을 자리). +""" + +import sys +from pathlib import Path + +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 ( # noqa: E402 + compute_cross_design, + stored_shoulder_extend, +) +from B06_Section.B06_Section_Router_Design import enforce_design_lines # noqa: E402 + + +def _ground(slope: float) -> list[dict]: + return [ + {"offset_m": o / 2, "elevation_m": round(100.0 + slope * o / 2, 4), "valid": True} + for o in range(-24, 25) + ] + + +LONG = { + "design_profiles": [ + { + "samples": [ + {"chainage_m": 0, "elevation_m": 100.4}, + {"chainage_m": 200, "elevation_m": 100.4}, + ] + } + ] +} + + +def test_저장값_없으면_0_음수_0() -> None: + assert stored_shoulder_extend(None) == { + "shoulder_extend_left_m": 0.0, + "shoulder_extend_right_m": 0.0, + } + got = stored_shoulder_extend({"shoulder_extend_left_m": 0.7, "shoulder_extend_right_m": -1}) + assert got == {"shoulder_extend_left_m": 0.7, "shoulder_extend_right_m": 0.0} + + +def test_저장_때_노견_확장_설계선이_다시_나옴(tmp_path: Path) -> None: + samples = _ground(0.35) + old = compute_cross_design(samples, 100.4, ground_type="soil", section_mode="left_cut") + # 브라우저 patch 가 사용자 값만 얹은 꼴 — 설계선 · 노견 끝은 옛 값 그대로 + stored = { + **old, + "shoulder_extend_left_m": 0.7, + "revet_adjust": {"outlet": {"x": 1.0, "d": None, "h": None, "m": None}}, + } + sections = [{"chainage_m": 100.0, "samples": samples, "design": stored}] + assert enforce_design_lines(LONG, sections, tmp_path) == 1 + design = sections[0]["design"] + assert design["road_edges"]["left"]["offset_m"] == old["road_edges"]["left"]["offset_m"] + 0.7 + assert design["road_edges"]["right"] == old["road_edges"]["right"] + assert design["shoulder_extend_left_m"] == 0.7 + assert design["revet_adjust"] == stored["revet_adjust"] # 사용자 값 승계 + assert design["design_line"] != old["design_line"] + + +def test_서버_재계산이_먼저_돌고_미교차_판정은_그_뒤() -> None: + src = (PROJECT_ROOT / "B06_Section" / "B06_Section_Server_Calc_Prebuild.py").read_text( + encoding="utf-8" + ) + assert src.index("enforce_design_lines, detail.get") < src.index("unclosed = any(")