feat(B04): 65-4 자동 배치 새 세월교 빈 월류 높이 = 담당 유역 필요 수심 · 시험 65

- _apply_new_defaults 가 유역 설계유량을 받아 새 세월교 빈 월류 높이를 채움(있는 값 · 유량 없음은 그대로)
- ford_height_m(서버) ↔ fordHeightM(화면) 같은 풀이 · 거울 시험
- 시험 test_65_ford_defaults: 유입 · 유출 칸 목록 · 없음 · 저장 키 · 칸 차례 · 정의 기본값 · 물넘이 키 · 집계표 · B06 읽기 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
2026-10-06 23:11:52 +09:00
co-authored by Claude Opus 5.5
parent d7faee1cd2
commit 4764977f9b
4 changed files with 291 additions and 3 deletions
+42 -3
View File
@@ -16,6 +16,7 @@ from __future__ import annotations
import asyncio
import logging
import math
from typing import Any
from uuid import UUID
@@ -41,7 +42,12 @@ from common_util.common_util_drainage_pipes import (
)
from common_util.common_util_json import LONLAT_DIGITS, round_floats
from common_util.common_util_route_geometry import StructureCandidate
from config.config_system import DRAINAGE_PIPE_MAX_SPACING_M, DRAINAGE_PIPE_MIN_SPACING_M
from config.config_system import (
DRAINAGE_PIPE_MAX_SPACING_M,
DRAINAGE_PIPE_MIN_SPACING_M,
FORD_MANNING_N,
FORD_SLOPE,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B04 Surface Drainage Basins"])
@@ -78,15 +84,20 @@ def _build(
carried = carry_facility_attributes(rebuilt, points or [])
# 되붙인 뒤에 추천을 얹는다 — 순서가 뒤바뀌면 승계된 사용자 값이 추천에 덮인다.
_apply_recommendations(detail, carried)
_apply_new_defaults([point for point, new in zip(carried, fresh) if new])
flows = {round(basin.chainage_m, 2): basin.design_flow_m3s for basin in detail.basins}
_apply_new_defaults([point for point, new in zip(carried, fresh) if new], flows)
return detail, carried
def _apply_new_defaults(points: list[PipePoint]) -> None:
def _apply_new_defaults(
points: list[PipePoint], flows: dict[float, float | None] | None = None
) -> None:
"""새로 놓인 관에만 등록부 기본값을 **빈 키에** 채움(PLAN 46-7 · 추천 뒤 — 추천 관경이 이김).
시설이 생길 때 기본값을 칸에 실제 값으로 넣어 두고 사용자가 고침 — 이미 있던 관의 빈 칸은 그대로
(옛 프로젝트 빈칸 = 미확정 유지 · 사용자 결정 2026-10-05).
세월교 빈 월류 높이 = 담당 유역 설계유량(`flows` · 측점 → ㎥/s)을 월류 폭으로 넘기는
필요 수심(65-4).
"""
types = structure_type_map()
for point in points:
@@ -100,9 +111,37 @@ def _apply_new_defaults(points: list[PipePoint]) -> None:
# 집수정 모양도 실제 값으로(54-39 · 54-36 기본 L형 — 화면 짝 `DEFAULT_BASIN_SHAPE`)
if options.get("inlet_type") == "집수정":
options.setdefault("inlet_structure", "L")
if point.facility == "ford_bridge" and "ford_height_m" not in options:
flow = (flows or {}).get(round(point.chainage_m, 2))
height = ford_height_m(flow, float(options.get("ford_width_m") or 0))
if height is not None:
options["ford_height_m"] = height
point.options = options or None
def ford_height_m(flow_m3s: float | None, width_m: float) -> float | None:
"""월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림 — 유량 · 폭이 없으면 None.
짝: `A00_Common/design/design_structure_ford_calc.fordHeightM`(+ `fordSection` 같은 풀이 ·
Manning n · S = `FORD_MANNING_N` · `FORD_SLOPE`) · 거울 시험 test_65_ford_defaults.
"""
if not flow_m3s or flow_m3s <= 0 or not math.isfinite(width_m) or width_m <= 0:
return None
root = math.sqrt(FORD_SLOPE)
def velocity(depth: float) -> float:
return (1 / FORD_MANNING_N) * ((width_m * depth) / (width_m + 2 * depth)) ** (2 / 3) * root
depth = ((flow_m3s * FORD_MANNING_N) / (width_m * root)) ** (3 / 5)
for _ in range(20):
nxt = flow_m3s / (width_m * velocity(depth))
if abs(nxt - depth) < 1e-6:
depth = nxt
break
depth = nxt
return math.ceil(depth * 100 - 1e-9) / 100
def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None:
"""확정 좌표로 되돌아온 관에 원래의 생성 사유를 다시 붙인다."""
by_chainage = {round(point.chainage_m, 2): point.source for point in points}