Files
Aislo/B06_Section/B06_Section_Schema.py
T
eomsangdonandClaude Opus 5 cd8d14f105 feat(B05·B06): 기슭막이 구간값 조정창 · 연동 기슭막이 옵션 · 가림 구간 선 정리
기슭막이 범위(길이·기준측점 전/후)를 B06 횡단도 조정창에서 지정하게 하고, 그
범위가 덮는 옆 측점의 기슭막이를 "연동" 파생물로 다루도록 체계를 세웠다.
값의 원천은 배수관 옵션(pipe_points) 하나다 — 같은 숫자를 B06 정본에 따로 담으면
B05 배수관 카드와 갈려 어느 쪽이 참인지 알 수 없다.

① 집수정 전/후 필드 신설 — inlet_basin_before_m·after_m(기본 각 1m), 길이 2m 유지.
   종방향 구간 계산을 revetSpanOfSpec·basinSpanOfSpec 한 곳으로 모아 2D 링크 판정과
   3D 스윕이 같은 값을 쓰게 했다.
② B06 조정창에 길이·전·후 행 — 기슭막이(유입·유출)·집수정 모두. 캐시를 고쳐 즉시
   보여주고 저장은 조작이 멎으면 한 번만 나간다(세부유역 재계산이 매번 돌지 않게).
   길이를 바꾸면 늘어난 몫만 지금 비율대로 나눠 담는다 — 총길이에서 비율로 다시
   계산하면 0.1m 반올림이 쌓여 5.0/5.0이 5.6/5.4로 어긋난다.
③ 링크 카드 선택 허용 — "소유 측점 통째 복사"에서 "위치 4축만 물려받고 원지반
   접합은 이 측점 지형으로 재계산"으로 바꿨다. 구조물 형식은 언제나 소유 측점 값이다.
   손대기 전에도 어긋나지 않게 소유 측점이 실제로 적용한 4축(revetShift)을 명시값으로
   물려준다.
④ 연동(측점별)·경사(전체 공통) 토글 — 연동 조작은 소유 측점 키에 실리고 그 연장이
   덮는 측점을 함께 다시 그린다. 경사는 3D 전용 스위치다(끄면 스윕 프레임 dz=0).
   횡단도까지 -dz로 밀면 노견에서 출발하는 성토선까지 내려가 노견과 벌어진다.
⑤ 가림 구간 선 정리 — 유입부 접속선과 유출부 접지 후 절토선을 2D·3D 모두에서
   빼되 기하는 남긴다. 면적·수량 계산은 건드리지 않았다(전면 개편 대상).
⑥ 집수정은 소유 측점 횡단도 하나에만 — 링크 카드에서 집수정·계류측 다단을 뺀다.
⑦ 3D 구간 겹침 정리 — 측점 간격이 구조물 연장보다 좁으면 두 구간 제어점이 번갈아
   서서 짧은 쪽(집수정 2m)이 잘려 보였다. 두 소유 측점의 중간에서 끊는다.
⑧ 조정창 정리 — 십자·9키를 맨 아래로, 창을 위쪽 기준 오버레이로(낮은 횡단도에서
   제목이 잘리던 문제), 연동·경사를 1행 토글 버튼으로, 스크롤 자리 유지.

파일 분리: B06_Section_UI_Cross_View_Structure.ts(조정창 배선),
B06_Section_Api_Culvert_Options.ts(구간값 저장) — 700줄 제한.
BUILD_VERSION 14 → 17(저장 코리도 만료).
tsc/ruff/prettier 통과, pytest 196 passed(기존 실패 1건 유지).
작업이력·자체검증 기록: docs/raw/PLAN.md §4-9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:26:35 +09:00

252 lines
11 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 ExtraWallCountsPatch(BaseModel):
"""다단 기슭막이 단 수 — 유출 성토부(outlet)·집수정 계류측(basin)."""
outlet: int = Field(default=0, ge=0, le=9)
basin: int = Field(default=0, ge=0, le=9)
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
extra_wall_counts: ExtraWallCountsPatch | None = None
# 연동 기슭막이 옵션(2026-08-24 사용자). 연동 해제는 측점별, 종단경사 반영은
# 기슭막이 한 벌 전체 공통이라 소유 측점에만 실린다.
revet_link_detached: bool | None = None
revet_follow_grade: bool | None = None
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)
# 암 경계선 기본 오프셋(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)