사용자 지시(2026-09-07): 「대신 경고부분은 삭제해주고 대신 각도를 사용자가 넣을수 있게 반영.
전체 공통으로 변경하는 경우에는 기본값 지정으로 하면 되지만 횡단도 하나만 변경하는 폼은
가져야함 / 개별 횡단도에는 암 절토 각도의 개별 수정 가능해야함」
**화면** — 암 측점 카드 아래에 「암 절토 68.2° ↺」 칸이 섬. 값을 넣으면 그 측점만 사면이
새 경사로 다시 그려지고 절토량도 따라 바뀜. ↺ 는 표준값으로 되돌림. 전체를 바꾸는 자리는
종전대로 좌측 [표준 횡단면 설정]임.
**계산에 넣는 자리는 한 곳씩** — 파이썬 `compute_cross_design(cut_slope_ratio=…)` ·
TS `computeCrossDesign({cutSlopeRatio})` 의 **그룹을 만든 바로 뒤**에서 경사비만 갈아 끼움.
부르는 쪽 9곳에서 표준값을 측점마다 복제하는 방식은 안 씀(한 곳만 빠져도 값이 조용히
사라지는 실패군). 기하·소단 코드는 한 줄도 안 건드림 — 25 확인대로 소단이 그 값을 읽어
서므로 **위치·개수가 새 경사를 저절로 따라옴**.
⚠ **경사는 나르는 값이 아니라 기하 입력임** — 계산 뒤에 키만 베껴 붙이면 설계선은 옛 경사로
그려지고 숫자만 새것이 됨(소단에서 겪은 자리). 그래서 재계산 세 경로(포장 강제·세월교 하강·
선형 재계산)와 브라우저 재계산·서버 프리뷰 **모두 계산 인자로** 넘김.
**되돌리기는 0 을 남김** — 세션에서 지우기만 하면 정본에 남은 옛 사용자 값이 되살아나
표준으로 못 돌아감. 0 = 「표준값을 씀」.
값의 길 — 세션 `cutslope`(등록표 한 줄) → 카드 입력 → [저장]·[확정]에서 `cross_patches`
(`design.cut_slope_ratio_user`) → 정본. `USER_TOUCHED_KEYS` 양쪽에 넣어 **표준을 바꿔도
개별로 고친 측점은 그대로** 둠(사용자 원문 끝줄).
시험 — 새 7건(넣은 경사가 실제로 그려짐 · 무릎 위 토사 경사는 그대로 · 0 은 되돌림 ·
재계산에도 남음 · **계산 전에 넘어감** · 각도↔경사비 · 칸은 암 측점에만),
거울 시험에 「암 절토 경사를 측점에서 바꿈」 한 갈래 추가, 7-3 회귀 한 줄 추가.
전체 **483 passed · 17 skipped**.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
330 lines
15 KiB
Python
330 lines
15 KiB
Python
"""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
|
|
# 이 측점만 쓰는 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
|
# None 이면 표준 횡단면 설정값을 쓴다.
|
|
cut_slope_ratio: float | None = Field(default=None, gt=0)
|
|
# 측구 생성 여부. 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 BasinAdjustPatch(BaseModel):
|
|
"""집수정 내부 치수와 기본 위치 대비 이동량."""
|
|
|
|
innerWidthM: float = Field(..., ge=1.0, le=2.0)
|
|
innerHeightM: float = Field(..., ge=1.0, le=2.0)
|
|
lateralM: float = Field(..., ge=0.0, le=10.0)
|
|
slopeM: float = Field(..., ge=0.0, le=10.0)
|
|
|
|
|
|
class WallAdjustPatch(BaseModel):
|
|
"""기슭막이 4축 조작값(좌우 x·상하 d·높이 h·재질 m). null = 자동."""
|
|
|
|
x: float = Field(default=0.0, ge=-10.0, le=10.0)
|
|
d: float | None = Field(default=None, ge=-10.0, le=10.0)
|
|
h: float | None = Field(default=None, ge=0.0, le=10.0)
|
|
m: str | None = None
|
|
|
|
|
|
class WallSpanPatch(BaseModel):
|
|
"""다단 기슭막이 한 단의 종방향 구간값(길이·기준측점 전/후 m — 2026-08-29)."""
|
|
|
|
length_m: float = Field(default=10.0, ge=0.0, le=200.0)
|
|
before_m: float = Field(default=5.0, ge=0.0, le=100.0)
|
|
after_m: float = Field(default=5.0, ge=0.0, le=100.0)
|
|
|
|
|
|
class ExtraWallCountsPatch(BaseModel):
|
|
"""다단 기슭막이 단 수 — 유출 성토부(outlet)·집수정 계류측(basin)."""
|
|
|
|
outlet: int = Field(default=0, ge=0, le=9)
|
|
basin: int = Field(default=0, ge=0, le=9)
|
|
|
|
|
|
class FordWallAdjustPatch(BaseModel):
|
|
"""세월교 측벽 한 매의 조작값 — 높이(자동이면 None)·좌우·상하(2026-08-25)."""
|
|
|
|
heightM: float | None = Field(default=None, ge=0, le=20)
|
|
lateralM: float = Field(default=0, ge=-20, le=20)
|
|
slopeM: float = Field(default=0, ge=-20, le=20)
|
|
|
|
|
|
class FordAdjustPatch(BaseModel):
|
|
"""세월교 측점의 유입·유출 측벽 조작값."""
|
|
|
|
inlet: FordWallAdjustPatch = Field(default_factory=FordWallAdjustPatch)
|
|
outlet: FordWallAdjustPatch = Field(default_factory=FordWallAdjustPatch)
|
|
|
|
|
|
class BoxSideAdjustPatch(BaseModel):
|
|
"""BOX암거 한쪽 끝의 조작값 — 길이 연장·표고(2026-08-25)."""
|
|
|
|
lengthM: float = Field(default=0, ge=0, le=50)
|
|
riseM: float = Field(default=0, ge=-20, le=20)
|
|
|
|
|
|
class BoxAdjustPatch(BaseModel):
|
|
"""BOX암거 측점의 좌·우 끝 조작값."""
|
|
|
|
left: BoxSideAdjustPatch = Field(default_factory=BoxSideAdjustPatch)
|
|
right: BoxSideAdjustPatch = Field(default_factory=BoxSideAdjustPatch)
|
|
|
|
|
|
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)
|
|
# 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
|
# **0 은 「표준값을 씀」**(되돌리기)이라 gt=0 이 아니라 ge=0 이다 — 0 이 와야
|
|
# 정본에 남은 옛 사용자 값이 지워진다.
|
|
cut_slope_ratio_user: float | None = Field(default=None, ge=0)
|
|
inlet_structure: Literal["auto", "revet", "I", "L", "U"] | None = None
|
|
basin_adjust: BasinAdjustPatch | None = None
|
|
# 기슭막이 4축 조작값 — 키는 역할("inlet"/"outlet"/"extra0"…/"bextra0"…).
|
|
# 세션 전용이던 값을 정본으로 올린다(2026-08-24 사용자: 3D는 확정 결과물).
|
|
revet_adjust: dict[str, WallAdjustPatch] | None = None
|
|
# 세월교 측벽 조작값(유입·유출) — 2026-08-25 사용자.
|
|
ford_adjust: FordAdjustPatch | None = None
|
|
# BOX암거 구체 조작값(좌·우 끝 길이·표고) — 2026-08-25 사용자.
|
|
box_adjust: BoxAdjustPatch | None = None
|
|
extra_wall_counts: ExtraWallCountsPatch | None = None
|
|
# 다단 기슭막이 단별 구간값 — 키는 벽 키("extra0"…/"bextra0"…). 기준벽 연장에
|
|
# 종속되지 않고 사용자가 단마다 넣는다(2026-08-29 사용자 확정).
|
|
extra_spans: dict[str, WallSpanPatch] | None = None
|
|
# 연동 기슭막이 옵션(2026-08-24 사용자). 연동 해제는 측점별, 종단경사 반영은
|
|
# 기슭막이 한 벌 전체 공통이라 소유 측점에만 실린다.
|
|
revet_link_detached: bool | None = None
|
|
revet_follow_grade: bool | None = None
|
|
# 카드 버튼 선택(2026-09-06) — 예전에는 버튼을 누를 때 서버가 계산·저장했다.
|
|
# 이제 조작은 세션 초안에 쌓이고 [저장]·[확정]에서 이 patch 로만 나간다.
|
|
ground_type: str | None = None
|
|
section_mode: str | None = None
|
|
ditch_side: str | None = None
|
|
ditch_type: str | None = None
|
|
paved: bool | None = None
|
|
two_stage_slope: bool | None = None
|
|
# 구조물이 선 측점의 폐회로 절·성토 면적(㎡) — 브라우저가 계산해 보낸다
|
|
# (2026-09-06 사용자 확정: 「일단은 브라우저 계산으로」). 서버는 초기값을 만들 때만
|
|
# 같은 코드를 Node 로 돌린다(`B06_Section_Server_Calc_Node.ts`).
|
|
cut_area_m2: float | None = Field(default=None, ge=0)
|
|
fill_area_m2: float | None = Field(default=None, ge=0)
|
|
cut_soil_area_m2: float | None = Field(default=None, ge=0)
|
|
cut_rock_area_m2: float | None = Field(default=None, ge=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
|
|
# 프로젝트 등록(B02)에서 정한 임도 종류 — B05 계획선 법정 기준의 출발점이다.
|
|
# 화면은 이 값을 읽기만 하고 고치지 않는다(2026-08-19 사용자 확정).
|
|
road_type: str | None = None
|
|
defaults: SectionOptionDefaults
|
|
# 표준 횡단면 설정 패널(토사/암/포장) 기본값. config STANDARD_CROSS_SECTION 사본.
|
|
standard_cross_section: dict[str, Any] = Field(default_factory=dict)
|
|
# 이 프로젝트에 **저장된** 표준 횡단면(사용자가 고쳐 [확정]한 값). 없으면 None(=기본값 그대로).
|
|
# 2026-09-07 추가 — 예전에는 브라우저가 이 값을 받을 길이 없어, 세션이 빈 새 탭에서
|
|
# **화면은 config 기본값으로, 서버는 저장분으로** 계산해 같은 측점이 갈렸다.
|
|
# 기존 `standard_cross_section`(기본값)은 그대로 두고 **한 칸만 더한다**.
|
|
stored_standard_cross_section: dict[str, Any] | None = None
|
|
# 암 경계선 기본 오프셋(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
|
|
# 측점별 소단 제원(chainage 키 → {width_m, interval_m, slope_deg}). 위와 같은 성격으로,
|
|
# 사용자가 구간에 놓은 소단을 확정 전에도 재계산에 반영한다(계획서 3-9).
|
|
# 값이 없는 측점은 소단 없음 — 종전 설계 그대로다.
|
|
berms: dict[str, dict[str, float]] | None = None
|
|
# 측점별 암 절토 경사비(chainage 키 → 1:n 의 n). 위와 같은 성격 — 확정 전 세션값을
|
|
# 재계산에 반영한다(2026-09-07). **0 은 「표준값을 씀」**(되돌리기)이다.
|
|
cut_slope_ratios: 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)
|