"""B05 페이지 설정 저장(448) — stage 2 params 에 얹을 값 · 종단 기준 갈아 끼우기 · 옛 스냅샷 걷기. 설계속도 · 지형 · 기준값은 **경고 기준만** 바꾼다 — 기하(base_pvi · 편집)는 그대로 · 종횡단 다시 만들기 없음. params 에는 사용자가 넣은 원값만 둔다(해석값 저장 금지 — `resolve_grade_options`). 시작 측점은 B05 표시 전용 — 저장만 하고 측점 열쇠 · 다른 페이지 라벨에 쓰지 않는다. """ from __future__ import annotations import json from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Engine_Grade import ( GradeDesignOptions, legal_plan_radius_min_m, resolve_grade_options, ) from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile from common_util.common_util_json import atomic_write_json #: params["options"] 안의 페이지 설정 칸 PAGE_OPTION_KEYS = ("design_speed_kph", "terrain_type") #: params 맨 위의 페이지 설정 칸(빈 값 = 법정값) PAGE_TOP_KEYS = ( "max_grade_pct", "min_vertical_radius_m", "min_tangent_length_m", "enforce_pipe_clearance", "station_display", ) _STORED_KEYS = ("max_grade_pct", "min_vertical_radius_m", "min_tangent_length_m") def params_patch(request: Any) -> dict[str, Any]: """요청 → stage 2 params 에 얹을 값(원값 그대로).""" return { "options": {key: getattr(request, key) for key in PAGE_OPTION_KEYS}, **{key: getattr(request, key) for key in PAGE_TOP_KEYS}, } def strip_page_settings(params: dict[str, Any] | None) -> dict[str, Any] | None: """페이지 설정 칸만 걷어 기본값으로 — stage 2 params 를 안 뜬 옛 스냅샷의 [초기화].""" if not isinstance(params, dict): return params stripped = {key: value for key, value in params.items() if key not in PAGE_TOP_KEYS} options = params.get("options") if isinstance(options, dict): stripped["options"] = {k: v for k, v in options.items() if k not in PAGE_OPTION_KEYS} return stripped def grade_class_of(road_type: str | None, params: dict[str, Any]) -> str: """기준 등급 — 노선 풀기가 남긴 값 · 없으면 B02 임도 종류(main → trunk · 그 밖 = work).""" stored = (params.get("options") or {}).get("grade_class") if stored: return str(stored) if road_type in ("main", "trunk"): return "trunk" return "fire" if road_type == "fire" else "work" def grade_options_of(road_type: str | None, params: dict[str, Any]) -> GradeDesignOptions: """params(사용자 원값) → 해석된 기준(작업임도 30 → 20 · 빈 칸 = 법정값).""" options = params.get("options") or {} speed = options.get("design_speed_kph") return resolve_grade_options( grade_class_of(road_type, params), terrain_type=str(options.get("terrain_type") or "special"), paved=bool(options.get("paved", False)), main_direction=str(options.get("main_direction") or "auto"), design_speed_kph=int(speed) if speed is not None else None, stored={key: params[key] for key in _STORED_KEYS if params.get(key) is not None} | {"enforce_pipe_clearance": params.get("enforce_pipe_clearance")}, ) def retarget_longitudinal( longitudinal: dict[str, Any], options: GradeDesignOptions ) -> tuple[dict[str, Any], dict[str, Any]]: """종단 정본의 정책 · 기준을 새 기준으로 — 저장 편집으로 다시 얹어 위반 · 요약만 새로. 정책 칸 = 최대 종단기울기 · 종단곡선 최소 R · 평면 최소 R(+ 있던 최소 L). 기하 칸은 그대로. 반환 = (선형, 요약 `{id, ...}`) · `longitudinal` 은 제자리에서 바뀐다. """ stored = longitudinal.get("profile_alignment") if not isinstance(stored, dict) or not stored.get("base_pvi"): raise ValueError("저장된 계획선 선형이 없어 기준을 바꿀 수 없습니다.") policy = dict(stored.get("policy") or {}) policy["max_grade_pct"] = options.max_grade_pct policy["min_vertical_radius_m"] = options.min_vertical_radius_m policy["min_plan_radius_m"] = legal_plan_radius_min_m( options.design_speed_kph, options.terrain_type ) if policy.get("min_curve_length_m"): policy["min_curve_length_m"] = options.min_curve_length_m stored["policy"] = policy profiles = longitudinal.get("design_profiles") previous = profiles[0] if isinstance(profiles, list) and profiles else {} direction = (previous.get("criteria") or {}).get("resolved_main_direction", "none") previous["criteria"] = {**options.as_dict(), "resolved_main_direction": direction} longitudinal["design_profiles"] = [previous, *(profiles or [])[1:]] alignment, profile = rebuild_alignment_profile(longitudinal, stored.get("edits")) longitudinal["profile_alignment"] = alignment longitudinal["design_profiles"][0] = profile return alignment, {"id": profile["id"], **profile["summary"]} def retarget_file( project_root: Path, longitudinal_file_path: str, options: GradeDesignOptions ) -> dict[str, Any] | None: """종단 정본 파일에 새 기준 — 요약을 돌려줌 · 계획선 선형이 없으면 None(파일 그대로).""" root = Path(project_root).resolve() path = (root / longitudinal_file_path).resolve() if root not in path.parents: raise ValueError("종단 데이터 경로가 프로젝트 밖을 가리킵니다.") if not path.is_file(): return None longitudinal = json.loads(path.read_text(encoding="utf-8")) if not (longitudinal.get("profile_alignment") or {}).get("base_pvi"): return None _, summary = retarget_longitudinal(longitudinal, options) atomic_write_json(path, longitudinal) return summary