From e40f4c9fd3268c72976871c8a89b9df3e54b406a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 02:59:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(b05=C2=B7b08):=20BOX=EC=95=94=EA=B1=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=B2=B4=20=ED=84=B0=ED=8C=8C=EA=B8=B0=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=89=EA=B7=A0=20=ED=84=B0=ED=8C=8C=EA=B8=B0=EA=B3=A0=20?= =?UTF-8?q?=EC=B9=B8=20=ED=95=98=EB=82=98=20=C2=B7=20=ED=86=A0=EA=B3=B5=20?= =?UTF-8?q?=EC=B6=95=EC=9C=BC=EB=A1=9C=EB=A7=8C=20=C2=B7=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=EC=9E=A1=EC=84=9D=20=EC=82=AC=EC=9C=A0(=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8=20=ED=8C=90=EC=A0=95=20=E2=91=A0=E2=91=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 등록부 trench_depth_m(평균 터파기고 · 기본값 없음 · 비면 사유) - 터파기 = (밑폭 버림 폭 + 여유 0.5×2 + 윗폭 1:0.5) ÷ 2 × 깊이 × 연장(울진 「암거구체 토공(2×2)」) · 되메우기 = 터파기 − 든 것(잡석·버림·구체) · 잔토처리 = 든 것 — destination earthwork(묶음에 안 듦 · 토공집계로만) - 울진 원문은 잔토에 구체 전체 · 되메우기 한쪽만 — 옹벽 식(터파기 − 든 것)으로 맞추고 사유에 적음 - 기초잡석 사유 「근거 둘(예제·울진 집계표)에는 잡석 줄이 없음 — 사용자 확정 3차 ② 로 세움」 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 10 +++ .../B08_Quantity_Engine_UnitQuantity.py | 16 +++++ .../B08_Quantity_Engine_UnitQuantity_Box.py | 65 +++++++++++++++++-- resources/tester/test_b08_box_culvert_unit.py | 29 +++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 6685edf0..a57a4069 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -289,6 +289,16 @@ "required": false, "phase": "detail" }, + { + "key": "trench_depth_m", + "label": "평균 터파기고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 구체 터파기·되메우기가 안 섬 — 원지반에서 터파기 바닥(기초잡석 밑)까지 · 토공집계로 감(2026-09-15 브레인 판정 ③)" + }, { "key": "wing_in", "label": "날개벽(유입)", diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 5dfce46a..88f1457a 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -520,6 +520,21 @@ def _append_section_trench( quantity.components.extend(Component(**row) for row in rows) +def _append_box_trench( + quantity: StructureQuantity, structure: dict[str, Any], thickness_m: float | None +) -> None: + """BOX암거 구체 터파기(토공 축) — 기초잡석과 **같은 두께**로 든 것을 뺌(2026-09-15).""" + if quantity.type_id != "box_culvert" or not quantity.components: + return + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Box import box_trench + + thickness = RUBBLE_BASE_THICKNESS_M if thickness_m is None else float(thickness_m) + found = box_trench(structure.get("options") or {}, max(thickness, 0.0)) + if found is not None: + rows, quantity.trench_depth_m = found + quantity.components.extend(rows) + + def build_table( structures: Iterable[dict[str, Any]], names: dict[str, str] | None = None, @@ -555,6 +570,7 @@ def build_table( if rubble is not None: quantity.components.append(rubble) _append_section_trench(quantity, item, observed, rubble_base_thickness_m) + _append_box_trench(quantity, item, rubble_base_thickness_m) quantity.unconfirmed = str(item.get("unconfirmed") or "") or missing_height_reason(item) quantities.append(quantity) if use_templates: diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py index 0657300b..12a49d5b 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Box.py @@ -41,9 +41,22 @@ NOTE_SHORING_SCAFFOLD = ( "(울진 2×2 「강재 3개월」) — 품셈 12-20 강관동바리 · 12-19 강관비계로 이음 ·" " 시종점 두 면은 개소당 한 번(원문은 m당 칸)" ) -NOTE_WING_EARTH = ( - "날개벽 수량 근거 없음(교본·실무 모두 · 판정 ⑤) — 날개벽 줄 안 섬" - " · 구체 터파기·되메우기는 아직 안 셈" +NOTE_WING = "날개벽 수량 근거 없음(교본·실무 모두 · 판정 ⑤) — 날개벽 줄 안 섬" +NOTE_RUBBLE = ( + "기초잡석 — 근거 둘(예제·울진 집계표)에는 잡석 줄이 없음 — 사용자 확정 3차 ② 로 세움" + "(버림이 선 구조물엔 잡석 · 옹벽과 같은 규칙)" +) +#: 구체 터파기 — 울진 「암거구체 토공(2×2)」: 밑폭 = 버림 폭 + 여유 0.5×2 · 옆면 1:0.5. +TRENCH_CLEARANCE_M = 0.5 +TRENCH_SIDE_SLOPE = 0.5 +NOTE_TRENCH_MISSING = ( + "평균 터파기고 칸이 비어 구체 터파기·되메우기가 안 섬 — 칸을 적으면 토공으로 섬" +) +NOTE_TRENCH = ( + "구체 터파기 — 밑폭·옆면은 울진 「암거구체 토공(2×2)」(여유 0.5 · 1:0.5)" + " · 깊이 = 평균 터파기고 칸 · 되메우기 = 터파기 − 든 것(기초잡석·버림·구체)" + " — 원문은 잔토에 구체 전체 · 되메우기 한쪽만이라 옹벽 식(터파기 − 든 것)으로 맞춤" + " · 토공집계로만 감(이중계상 방지)" ) @@ -93,7 +106,7 @@ def box_culvert(options: dict[str, Any]) -> tuple[list[Component], list[str]]: ), ("비계", "㎡", 2 * outer_h, f"외벽 양면 {outer_h:g}×2", ""), ] - notes: list[str] = [SOURCES, NOTE_FORMWORK, NOTE_SHORING_SCAFFOLD, NOTE_WING_EARTH] + notes: list[str] = [SOURCES, NOTE_FORMWORK, NOTE_SHORING_SCAFFOLD, NOTE_WING, NOTE_RUBBLE] if math.isclose(width, 2.0) and math.isclose(height, 2.0): per_m += [ ("이형철근", "kg", kg, "울진 기번3 구조도 2×2 관측(SD40)", size) @@ -122,4 +135,48 @@ def box_culvert(options: dict[str, Any]) -> tuple[list[Component], list[str]]: component.basis += ( f" + 시종점 두 면 {outer_w:g}×{outer_h:g}×2 = {ends:.4g}(개소당 한 번)" ) + if _num(options.get("trench_depth_m")) <= 0: + notes.append(NOTE_TRENCH_MISSING) return components, notes + + +def box_trench( + options: dict[str, Any], rubble_thickness_m: float +) -> tuple[list[Component], float] | None: + """구체 터파기·되메우기·잔토(토공 축) — 평균 터파기고가 비면 `None`. + + 기초잡석 두께는 원단위표와 **같은 값**을 받음(`build_table` 의 두께) — 두 벌로 안 셈. + """ + depth, length = _num(options.get("trench_depth_m")), _num(options.get("length_m")) + outer_w = _num(options.get("body_width_m")) + 2 * _num(options.get("wall_thickness_m")) + outer_h = _num(options.get("body_height_m")) + 2 * _num(options.get("slab_thickness_m")) + blinding = _num(options.get("blinding_thickness_m")) + if depth <= 0 or length <= 0 or outer_w <= 0: + return None + base_w = outer_w + 2 * BLINDING_EXTRA_M + bottom = base_w + 2 * TRENCH_CLEARANCE_M + top = bottom + 2 * TRENCH_SIDE_SLOPE * depth + excavation = (bottom + top) / 2 * depth + body = min(max(depth - rubble_thickness_m - blinding, 0.0), outer_h) + filled = base_w * (rubble_thickness_m + blinding) + outer_w * body + notes: list[str] = [] + rows = [ + ( + "터파기", + excavation, + f"(밑폭 {bottom:.2f}(버림 폭 {base_w:g} + 여유 {TRENCH_CLEARANCE_M:g}×2) + 윗폭" + f" {top:.2f}(1:{TRENCH_SIDE_SLOPE:g})) ÷ 2 × 깊이 {depth:g} · {NOTE_TRENCH}", + ), + ( + "되메우기", + excavation - filled, + f"터파기 − 든 것 {filled:.3f}(버림 폭 × (잡석 {rubble_thickness_m:g}" + f" + 버림 {blinding:g}) + 구체 폭 {outer_w:g} × {body:.2f})", + ), + ("잔토처리", filled, "터파기 − 되메우기"), + ] + return [ + Component(name, "㎥", amount * length, destination, f"{basis} × 연장 {length:g}m") + for name, amount, basis in rows + if (destination := routed(name, notes)) + ], depth diff --git a/resources/tester/test_b08_box_culvert_unit.py b/resources/tester/test_b08_box_culvert_unit.py index 5f98a5fe..e8cb6387 100644 --- a/resources/tester/test_b08_box_culvert_unit.py +++ b/resources/tester/test_b08_box_culvert_unit.py @@ -86,6 +86,35 @@ def test_치수를_안_적으면_기본값으로_서되_미확정() -> None: assert not next(r for r in stored if r["type_id"] == "box_culvert").get("unconfirmed") +def test_구체_터파기는_평균_터파기고로_토공_축에만_선다() -> None: + """브레인 판정(2026-09-15 ③) — 칸 하나(평균 터파기고) · 구조물 터파기는 토공집계로만(이중계상). + + 밑폭 = 버림 폭 2.8 + 여유 0.5×2 · 옆면 1:0.5(울진 「암거구체 토공(2×2)」) · 깊이 0.8 · + 든 것 = 버림 폭 × (기초잡석 0.2 + 버림 0.1) + 구체 폭 × (0.8 − 0.3) · 되메우기 = 터파기 − 든 것. + """ + table = build_table( + [{"type_id": "box_culvert", "options": {**ULJIN_2X2, "trench_depth_m": 0.8}}] + ) + box = table["structures"][0] + earth = {c["name"]: c for c in box["components"] if c["destination"] == "earthwork"} + assert earth["터파기"]["amount"] == pytest.approx((3.8 + 4.6) / 2 * 0.8 * 10) + filled = 2.8 * (0.2 + 0.1) + 2.6 * (0.8 - 0.3) + assert earth["잔토처리"]["amount"] == pytest.approx(filled * 10) + assert earth["되메우기"]["amount"] == pytest.approx(((3.8 + 4.6) / 2 * 0.8 - filled) * 10) + assert box["trench_depth_m"] == 0.8 + # 잡석 사유 — 근거 둘에는 없고 사용자 확정으로 섬(브레인 판정 ①). + assert any("잡석" in note and "사용자 확정 3차" in note for note in box["notes"]) + # 묶음(내역 줄)에는 터파기가 안 들어감. + row = next( + r for r in build_handoff(unit_quantity_table=table)["work_items"] if "BOX" in r["name"] + ) + assert not [p for p in row["composite_parts"] or [] if "터파기" in str(p.get("name"))] + # 칸이 비면 터파기가 안 서고 사유. + empty = build_table([{"type_id": "box_culvert", "options": ULJIN_2X2}])["structures"][0] + assert not [c for c in empty["components"] if c["destination"] == "earthwork"] + assert any("평균 터파기고" in note for note in empty["notes"]) + + def test_인계_묶음이_조각을_세운다() -> None: table = build_table( facility_structures(