feat(B08): 성토고를 사면표에 실어 수평 규준틀 개소를 셈

㉔. 확정 7(준비공 3줄)의 마지막 하나가 닫힘.

- 사면길이 유도가 이미 성토 사면을 재고 있어 **성토 조각의 수직 낙차 합**이 곧
  성토고임. `StationSlope.fill_height_m` 으로 실어 사면표까지 옮김.
  ⚠ 좌우가 다르면 **큰 쪽**을 씀 — 「중심점 성토고 5m 이상」 판정은 가장 높은
  쪽이 기준이고 양쪽을 더하면 실제보다 두 배가 됨.
- 수평 규준틀은 **성토고 5m 이상 측점마다 한 개소**(11-3 [주]①).
  ⚠ 비탈규준틀(「10m 이상 20m마다」)과 **기준이 다름** — 이쪽은 지점 조건이라
  간격이 없음. 두 기준을 같은 식으로 쓰면 조용히 틀림.
- 성토고 칸이 아예 없으면 0 으로 때우지 않고 미확보로 둠. 성토고가 다 낮으면
  0 개소가 **값**임 — 「못 셈」과 다름. 셋을 시험으로 갈라 못 박음.

실노선 확인 — 비탈 규준틀 31개소(617.475m ÷ 20 + 1) · 수평 규준틀 39개소.

검증 — 준비공 18건 통과, 전체 647 passed, tsc 오류 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 04:52:43 +09:00
co-authored by Claude Opus 5
parent d0978d2422
commit 089f80b550
3 changed files with 48 additions and 14 deletions
+35 -14
View File
@@ -72,13 +72,23 @@ def batter_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[
return count, notes
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int, list[str]]:
"""수평 규준틀 — **중심점 성토 높이 5m 이상**에 설치(품셈 11-3 [주]①).
def level_frame_count(slope_rows: Iterable[dict[str, Any]]) -> tuple[int | None, list[str]]:
"""수평 규준틀 — **성토고 5m 이상 측점마다** 한 개소(품셈 11-3 [주]①).
성토고는 사면표에 없다 — 그 값을 받기 전에는 **0 으로 때우지 않고 미확보**로 둔다.
「비탈길이 10m 이상 20m마다」인 비탈규준틀과 **기준이 다르다** — 이쪽은 **지점 조건**이라
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다.
⚠ 성토고 칸이 아예 없으면 **0 으로 때우지 않고** 미확보로 둔다 — 「없음」과 다르다.
"""
return 0, [
"중심점 성토고 5m 이상 지점에 설치(품셈 11-3 [주]①)인데 성토고가 이 표에 없어 개소를 못 셈"
rows = list(slope_rows)
if not rows:
return None, ["사면표가 없어 성토고를 못 봄"]
if all("fill_height_m" not in row for row in rows):
return None, ["성토고가 사면표에 없어 개소를 못 셈 (품셈 11-3 [주]① 「성토고 5m 이상」)"]
tall = [
row for row in rows if float(row.get("fill_height_m") or 0.0) >= LEVEL_MIN_FILL_HEIGHT_M
]
return len(tall), [
f"성토고 {LEVEL_MIN_FILL_HEIGHT_M:g}m 이상 측점 {len(tall)}곳 (품셈 11-3 [주]①)"
]
@@ -119,15 +129,7 @@ def preparation_rows(
"work_item_code": "FP-09-21",
},
_batter_frame_row(list(slope_rows)),
{
"group": "준비공",
"item": "수평 규준틀",
"unit": "개소",
"amount": None,
"status": STATUS_PENDING,
"reason": level_frame_count(())[1][0],
"work_item_code": "FP-11-03",
},
_level_frame_row(list(slope_rows)),
]
@@ -179,6 +181,25 @@ def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
}
def _level_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
"""수평 규준틀 한 줄. 성토고를 못 보면 미확보, 보면 개소가 선다."""
count, notes = level_frame_count(slope_rows)
return {
"group": "준비공",
"item": "수평 규준틀",
"unit": "개소",
"amount": float(count) if count is not None else None,
"status": STATUS_READY if count is not None else STATUS_PENDING,
"reason": "; ".join(notes)
+ (
" · 재료량은 품셈 11-3 [주]④ 「설계수량에 따른다」라 미확보"
if count is not None
else ""
),
"work_item_code": "FP-11-03",
}
def erosion_rows(structures: Iterable[dict[str, Any]] = ()) -> list[dict[str, Any]]:
"""사방공 줄 — 이 노선에 사방 시설이 **있을 때만** 값이 선다."""
found = sorted(
@@ -69,6 +69,8 @@ class SlopeAreaRow:
chainage_m: float
distance_m: float = 0.0
berm_width_m: float = 0.0
# 성토고(m) — 수평 규준틀 개소 판정(품셈 11-3 [주]① 「성토고 5m 이상」)이 쓴다.
fill_height_m: float = 0.0
unclosed: bool = False
lengths: dict[str, float] = field(default_factory=dict)
areas: dict[str, float] = field(default_factory=dict)
@@ -102,6 +104,7 @@ def build_rows(
row = SlopeAreaRow(
chainage_m=slope.chainage_m,
berm_width_m=slope.berm_width_m,
fill_height_m=slope.fill_height_m,
unclosed=slope.unclosed,
)
for series, faces in SERIES:
@@ -153,6 +156,7 @@ def build_table(
"chainage_m": row.chainage_m,
"distance_m": row.distance_m,
"berm_width_m": row.berm_width_m,
"fill_height_m": row.fill_height_m,
"unclosed": row.unclosed,
"lengths": row.lengths,
"areas": row.areas,
@@ -59,6 +59,10 @@ class StationSlope:
cut_length_m: float = 0.0
fill_length_m: float = 0.0
berm_width_m: float = 0.0
# 성토고(m) — 성토 사면 조각들의 **수직 낙차 합**. 노면 끝에서 원지반까지 내려간 높이다.
# ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은
# 가장 높은 쪽이 기준이고, 양쪽을 더하면 실제보다 두 배가 된다.
fill_height_m: float = 0.0
segments: tuple[SlopeSegment, ...] = ()
# 사면이 샘플 범위 끝까지 원지반을 못 만나 **면적이 잘린** 측점.
# 설계 엔진이 `slope_unclosed` 로 이미 경고하는 값을 그대로 물고 온다. 잘린 측점은
@@ -204,10 +208,15 @@ def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope:
for side in ("left", "right"):
segments.extend(_side_segments(design, side, ratios))
berm = design.get("berm") or {}
fill_by_side = {
side: sum(abs(s.rise_m) for s in segments if s.role == "fill" and s.side == side)
for side in ("left", "right")
}
return StationSlope(
chainage_m=float(chainage_m),
cut_length_m=sum(s.length_m for s in segments if s.role == "cut"),
fill_length_m=sum(s.length_m for s in segments if s.role == "fill"),
fill_height_m=max(fill_by_side.values(), default=0.0),
berm_width_m=_num(berm.get("width_m")) or 0.0,
segments=tuple(segments),
unclosed=bool(design.get("slope_unclosed")),