사용자 확정(2026-09-07): 「연암과 경암 / 발파암과 리핑암 선택은 횡단도에서 선택 안함. 사유는 향후 설계내역에서 설계자가 직접 비율로 지정하기로 함. 암반 지정과 범위 애매모호한 경우가 있어. 실무자는 그렇게 하기로 판단함. 대신 경고부분은 삭제해주고 대신 각도를 사용자가 넣을수 있게 반영.」 **왜 폐기인가** — 판정이 서려면 측점마다 암질(연암·경암)을 못 박아야 하는데, 실무에서 암반 지정·범위가 애매해 그 못 박음 자체가 틀린 전제였음. 암 비율은 **설계내역 단계에서 설계자가 비율로** 넣음. 기준이 없으니 경고도 없음. 되돌린 것 — 1e596846(암질 선택) + 973128f7(별표2 경고). 카드 경고 배지 · 판정 모듈 · config 범위표 · context 필드 · 표준 패널 암질 칸 전부 사라짐. ⚠ **남긴 것** — `cut_slope_segments`(구간별 경사)는 지우지 않음. 소단 기하가 그 위에 서 있고 구간 경사를 아는 값이라 뒤에 쓸 자리가 있음. 4e7a4bf7(사면 구간 재료를 그린 경사비로 가름 — 무릎 위 라벨 13구간 오류 수정)도 남김. 검사와 무관하게 옳은 고침임. 시험 4건 — 되살아나지 않게 막는 쪽으로 다시 씀(판정 모듈 없음 · 카드 경고 없음 · 암질 고르는 자리 없음 · **구간별 경사값은 남아 있음**). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
320 lines
15 KiB
Python
320 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
|
|
# 측구 생성 여부. 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)
|
|
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
|
|
|
|
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)
|