feat(B05,B06): 물넘이 3D 파임 + 독립 기슭막이 횡단·3D + 포장 기본값·표준횡단 반영

2026-08-28 사용자 지시 4건.

① 물넘이 파임을 3D 코리도에도 반영
   classifyStation 이 노면·노견 조각 표고를 횡단도와 같은 산식(fordDeckElevationAt)
   으로 내린다. 노면 밖 비탈은 그대로라 노견 끝에 수직 단차가 선다. BUILD_VERSION 85.

② 표준횡단면 저장분을 포장 재계산에 싣는다
   확정 때 종단 정본 options 에 실린 standard_cross_section 을 읽어
   enforce_pavement_ranges / attach_default_designs 에 넘긴다. 없으면 config 기본값.

③ 포장 구간 기본값 10 / 5 / 5 (기슭막이와 같은 출발값).

④ 독립 기슭막이
   - 측점: is_station_planting_type() 규칙 신설(A군 + D군 구간형). 구간형은
     시작·기준·종료에 측점을 심는다 — 길이가 길수록 횡단도가 여러 장 나온다.
     구조물 재이관 차단도 같은 규칙을 본다(기타 고스트 방지).
   - 서버: B06_Section_Engine_Revetment 신설 — 구간 안 측점 전부에 section.revetment.
   - 횡단도: B06_Section_UI_Cross_Revetment 신설 — 벽 상단 = 성토면 끝(설계선-지반
     교차점), 아래로 높이 + 근입 0.5m, 전면 1:0.3(교본 7-3). 설치 측은 사용자가
     좌/우로 고른다(레지스트리 side 옵션 신설 — 자동 판정 없음).
   - 3D: 같은 폴리곤을 기준측점 전/후만큼 스윕. BUILD_VERSION 86 + 구조물 해시에
     물넘이·기슭막이 제원 추가.

검증: pytest 241 passed / 7 skipped(신규 4건). 공용 브라우저 실측 —
물넘이 240m 투입 시 3D 노면이 계획고보다 0.38~0.42m 아래(대조군 200m 정상),
기슭막이 225~255 투입 시 측점 23→25·횡단 카드 3장·3D revet 솔리드 3개(31링).
임시 데이터는 원래 정본으로 복구했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 12:54:45 +09:00
co-authored by Claude Opus 5
parent 80c2a265f9
commit e371c2754a
16 changed files with 409 additions and 22 deletions
+19 -1
View File
@@ -91,8 +91,23 @@ _USER_TOUCHED_KEYS = (
)
def stored_standard_cross_section(longitudinal_row: dict[str, Any] | None) -> dict[str, Any] | None:
"""확정 때 저장해 둔 표준횡단면 설정값(없으면 None = config 기본값).
사용자가 「표준 횡단면 설정」에서 고친 값은 확정 시 종단 정본 options에 실린다.
포장 강제 재계산도 같은 값을 써야 카드가 패널과 어긋나지 않는다(2026-08-28).
"""
options = (longitudinal_row or {}).get("data") or {}
options = options.get("options") if isinstance(options, dict) else None
standard = options.get("standard_cross_section") if isinstance(options, dict) else None
return standard if isinstance(standard, dict) else None
def enforce_pavement_ranges(
longitudinal: dict[str, Any], cross_sections: list[dict[str, Any]], project_root: Path
longitudinal: dict[str, Any],
cross_sections: list[dict[str, Any]],
project_root: Path,
standard: dict[str, Any] | None = None,
) -> int:
"""포장 구간·물넘이 범위 안 측점을 포장으로 맞춘다 — 저장분이 비포장이어도 그렇다.
@@ -120,6 +135,7 @@ def enforce_pavement_ranges(
ditch_side=design.get("ditch_side"),
ditch_type=str(design.get("ditch_type") or "standard"),
paved=True,
standard=standard,
rock_boundary_offset_m=design.get(
"rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
),
@@ -173,6 +189,7 @@ def attach_default_designs(
longitudinal: dict[str, Any],
cross_sections: list[dict[str, Any]],
project_root: Path | None = None,
standard: dict[str, Any] | None = None,
) -> None:
modes = default_section_modes(longitudinal)
pavement = pavement_suggestions(longitudinal)
@@ -190,6 +207,7 @@ def attach_default_designs(
section_mode=modes.get(round(chainage, 3), "left_cut"),
# 포장은 사용자 구간 지정만 켠다 — 경사 제안은 경고로만 남는다(2026-08-28).
paved=paved_at(chainage, paved_ranges),
standard=standard,
rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M,
)
design.update(status="provisional", pavement_suggested=suggested)