"""B06 종횡단 조회·확정 응답 검증 모델.""" from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field class SectionRegenerateRequest(BaseModel): """표시 옵션의 횡단 반폭 변경에 따른 종횡단 재생성 요청.""" cross_half_width_m: float = Field(..., gt=0) class CompanyStandardProject(BaseModel): """같은 회사의 표준횡단 설정값 보유 프로젝트 항목(조회용).""" project_id: str name: str class CompanyStandardListResponse(BaseModel): """같은 회사에서 설계값을 불러올 수 있는 프로젝트 목록.""" status: str = "success" projects: list[CompanyStandardProject] class CompanyStandardResponse(BaseModel): """특정 프로젝트의 표준횡단 설정값(미리보기·적용용, 회사 스코프).""" status: str = "success" project_id: str standard_cross_section: dict[str, Any] class CrossDesignRequest(BaseModel): """측점 하나의 표준횡단 설계(지반유형·단면유형) 지정 요청. 버튼 클릭 즉시 절·성토 단면적을 계산해 잠정치로 저장한다. 편절편성은 절토측이 측구측이라 ditch_side가 무시되고, 양절·양성에서만 배수 방향으로 사용된다. """ chainage_m: float = Field(..., ge=0) ground_type: Literal["soil", "ripping_rock", "blasting_rock"] section_mode: Literal["left_cut", "right_cut", "both_cut", "both_fill"] ditch_side: Literal["left", "right"] | None = None # 측구 형식: 일반(사다리꼴)/L형. L형은 암 구간에서만 허용된다. ditch_type: Literal["standard", "l_type"] = "standard" # 포장 중첩 여부 — 횡단경사·포장층만 포장 그룹 값으로 계산한다. paved: bool = False # 암 경계선 오프셋(m, 지면선 기준 하향 음수). 암 지반 2단계 절토 무릎 계산에 쓴다. rock_boundary_offset_m: float | None = None # 암 지반 2단계 경사(암반 경계 아래=암 경사, 위=토사 경사) 적용 여부. 토글로 해제 가능. two_stage_slope: bool = True # 측구 생성 여부. None=자동 판정(측구측 절토면만 생성), True/False=수동 override. ditch_enabled: bool | None = None # B06 설정 패널 편집값(STANDARD_CROSS_SECTION 형태). 요청값 → config 기본값 순. standard_cross_section: dict[str, Any] | None = None class CrossDesignResponse(BaseModel): """측점 표준횡단 설계 계산 결과(잠정치).""" status: str = "success" chainage_m: float design: dict[str, Any] class CrossSectionPatch(BaseModel): """확정 시 측점별 data.design에 병합할 프론트 세션 보관값.""" chainage_m: float = Field(..., ge=0) # 암 경계선 오프셋(m, 지면선 기준 하향 음수 — 계획선 아님). 암 지반 측점만 의미 있다. rock_boundary_offset_m: float | None = None # 측점별 표시 반폭(m) — 카드 개별 조절값. 전역 반폭과 다를 때만 실린다(2026-08-06). display_half_width_m: float | None = Field(default=None, gt=0) class SectionConfirmRequest(BaseModel): """종횡단 확정 요청. 확정 시 저장할 표준 횡단면 설정값(선택)을 함께 받는다.""" # B06 표준 횡단면 설정 패널 편집값(토사/암/포장). longitudinal_sections.data.options에 저장. standard_cross_section: dict[str, Any] | None = None # 측점별 세션 보관값(암 경계선 오프셋 등) — 확정 시점에 일괄 DB 병합. cross_patches: list[CrossSectionPatch] | None = None # 프론트가 계산한 유토곡선 결과. longitudinal_sections.data.mass_haul에 저장한다. # 계산은 프론트 전담(네트워크 왕복 회피)이라 백엔드는 검산 없이 스냅샷으로 보관한다. mass_haul: dict[str, Any] | None = None class SectionConfirmResponse(BaseModel): """종횡단 확정 결과.""" status: str = "success" project_id: str route_id: int confirmed: bool = True class SectionOptionDefaults(BaseModel): """config에서 읽은 종횡단 생성 기본 옵션.""" station_interval_m: float cross_half_width_m: float cross_sample_interval_m: float long_sample_interval_m: float vertical_exaggeration: float class EarthworkConversionFactor(BaseModel): """지반유형 하나의 토량환산계수(둘 다 자연상태 기준 배율). loose = 흐트러진 상태 / 자연상태(팽창률), compacted = 다져진 상태 / 자연상태(다짐률). 정의처는 config.config_system.EARTHWORK_CONVERSION_FACTORS 한 곳뿐이다. """ loose: float = Field(..., gt=0) compacted: float = Field(..., gt=0) class HaulEquipmentLimit(BaseModel): """평균운반거리 상한 하나(m). max_distance_m=None이면 상한 없음(나머지를 전부 받는다). 정의처는 config.config_system.EARTHWORK_HAUL_EQUIPMENT_LIMITS_M 한 곳뿐이다. 프론트(유토곡선 토량 분배)는 이 값을 받아 쓰고 사본을 두지 않는다. """ key: str max_distance_m: float | None = None class SectionContextResponse(BaseModel): """B06 진입 시 필요한 확정 경로 컨텍스트와 기본 옵션.""" project_id: str route_id: int | None = None filter_key: str | None = None method: str | None = None smooth: bool | None = None crs_epsg: int | None = None defaults: SectionOptionDefaults # 표준 횡단면 설정 패널(토사/암/포장) 기본값. config STANDARD_CROSS_SECTION 사본. standard_cross_section: dict[str, Any] = Field(default_factory=dict) # 암 경계선 기본 오프셋(m)과 상/하 제어 스텝(m). rock_boundary_default_offset_m: float = -0.5 rock_boundary_step_m: float = 0.1 # 지반유형별 토량환산계수. 유토곡선 계산은 프론트가 이 값으로 수행한다. earthwork_conversion: dict[str, EarthworkConversionFactor] = Field(default_factory=dict) # 평균운반거리별 운반장비 경계. 유토곡선의 토량 분배가 이 값으로 장비를 고른다. haul_equipment_limits: list[HaulEquipmentLimit] = Field(default_factory=list) # 자연방토 판정 경사(rise/run). 성토측 자연 지반이 이보다 가파르면 흙이 스스로 흘러내려 # 운반비를 세지 않는다. 정의처는 config_system.NATURAL_SPOIL_MIN_GROUND_SLOPE 한 곳뿐이다. natural_spoil_min_ground_slope: float | None = None class SectionSummaryResponse(BaseModel): """종횡단 요약 조회 결과.""" status: str = "success" project_id: str route_id: int longitudinal: dict[str, Any] | None = None length_m: float | None = None cross_section_count: int = 0 class SectionDetailResponse(BaseModel): """종단·횡단 SVG 렌더링에 필요한 원시 샘플 데이터.""" longitudinal: dict[str, Any] cross_sections: list[dict[str, Any]] # 유토곡선 balloon을 사용자가 끌어 옮긴 위치(띠 번호 -> [dx, dy], px). # longitudinal_sections.data.mass_haul.balloon_offsets에 확정 시점에 저장된 값이며, # 브라우저가 바뀌어도 같은 자리에 뜨도록 진입 시 프론트 캐시의 씨앗으로 내려보낸다. balloon_offsets: dict[str, list[float]] | None = None class CrossDesignPreviewRequest(BaseModel): """계획선 편집 델타로 **전 측점 횡단 설계를 다시 계산**해 달라는 요청(저장 없음). B05에서 계획고를 끌면 측점별 횡단 단면적이 함께 바뀌어야 횡단 기준 유토곡선이 따라 움직인다. 측점마다 따로 부르면 수십 번 왕복하므로 한 번에 계산한다. 사용자가 이미 지정한 지반유형·단면유형·측구·암 경계는 그대로 유지하고 **계획고만 새 선형으로 바꿔** 재계산한다. """ model_config = ConfigDict(extra="forbid") station_offsets: dict[str, float] = Field(default_factory=dict) curve_radii: dict[str, float] = Field(default_factory=dict) # B06 설정 패널 편집값(있으면 그 표준단면으로 계산). standard_cross_section: dict[str, Any] | None = None # True면 설계 전체(설계선 좌표 포함)를 돌려준다 — B06 진입 시 stale 설계 일괄 재계산용. # B05 편집 프리뷰(기본 False)는 유토곡선 필드만 받아 응답을 가볍게 유지한다. full_designs: bool = False # 측점별 암 경계 오프셋 세션값(chainage 키 → m). B06이 확정 전 세션에만 들고 있는 # 오프셋을 재계산에 반영하기 위한 값 — 없으면 DB 저장분을 쓴다. rock_boundary_offsets: dict[str, float] | None = None def edits(self) -> dict[str, Any]: return {"station_offsets": self.station_offsets, "curve_radii": self.curve_radii} class CrossDesignPreviewResponse(BaseModel): """계획선 편집 프리뷰 결과 — **바뀌는 것만** 담는다. 계획고를 끄는 동안 오가는 값이라 **유토곡선이 실제로 읽는 설계 필드만** 담는다. 지반선 샘플·설계선 좌표·선형 구조까지 실으면 1MB가 넘어 편집이 굼떠지고 (2026-08-03 사용자 지적), 계획선 자체는 화면이 같은 규칙으로 이미 계산해 들고 있다. """ status: str = "ok" designs: list[dict[str, Any]] = Field(default_factory=list)