From 089f80b55098345b90f0e6f0a63cc8a4165dd83b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 04:52:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=84=B1=ED=86=A0=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EB=A9=B4=ED=91=9C=EC=97=90=20=EC=8B=A4?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=ED=8F=89=20=EA=B7=9C=EC=A4=80=ED=8B=80=20?= =?UTF-8?q?=EA=B0=9C=EC=86=8C=EB=A5=BC=20=EC=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ㉔. 확정 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) --- .../B08_Quantity_Engine_Preparation.py | 49 +++++++++++++------ B08_Quantity/B08_Quantity_Engine_SlopeArea.py | 4 ++ .../B08_Quantity_Engine_SlopeLength.py | 9 ++++ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Preparation.py b/B08_Quantity/B08_Quantity_Engine_Preparation.py index 35c64a9f..bcb52002 100644 --- a/B08_Quantity/B08_Quantity_Engine_Preparation.py +++ b/B08_Quantity/B08_Quantity_Engine_Preparation.py @@ -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( diff --git a/B08_Quantity/B08_Quantity_Engine_SlopeArea.py b/B08_Quantity/B08_Quantity_Engine_SlopeArea.py index d5df001e..47d92d1e 100644 --- a/B08_Quantity/B08_Quantity_Engine_SlopeArea.py +++ b/B08_Quantity/B08_Quantity_Engine_SlopeArea.py @@ -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, diff --git a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py index 8f6a3d28..e7497743 100644 --- a/B08_Quantity/B08_Quantity_Engine_SlopeLength.py +++ b/B08_Quantity/B08_Quantity_Engine_SlopeLength.py @@ -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")),