diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py index 0e7ac65d..7acc15f2 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py @@ -221,7 +221,9 @@ def structure_earthwork_rows( for structure in unit_quantity_table.get("structures") or []: options = structure.get("options") or {} height = float(structure.get("height_m") or options.get("height_m") or 0.0) - band = _depth_band(height + _foundation_depth(options)) + # 단면으로 판 깊이가 있으면 그것(옹벽은 기초분만 팜 · 비탈분 제외) — 없으면 직고 + 기초 깊이. + dug = structure.get("trench_depth_m") + band = _depth_band(float(dug) if dug else height + _foundation_depth(options)) ground = structure.get("ground_type") or None ground_basis = str(structure.get("ground_type_basis") or "") for component in structure.get("components") or []: diff --git a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py index be3d7eda..2ce266f4 100644 --- a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py +++ b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py @@ -151,23 +151,101 @@ def billing_of( return str(found.get("unit") or "개소"), scale +#: 단면으로 터파기를 세운 줄의 사유 — 표 사유·구조물도 그림이 **같은 말**(브레인 판정 2026-09-14). +SECTION_TRENCH_NOTE = "터파기는 기초 폭 + 양쪽 여유로 수직 — 지반선이 제원에 없어 비탈분 제외" + + def earthwork_missing_note(entry: dict[str, Any]) -> str | None: - """관측 줄에 토공 줄이 없으면 한 줄 — 표 사유·구조물도 그림이 **같은 말**을 씀.""" + """관측 줄에 토공 줄이 없고 단면으로도 못 세우면 한 줄 — 표 사유·구조물도 그림이 같은 말.""" if any(item.get("destination") == "earthwork" for item in entry.get("components") or []): return None + if (entry.get("section") or {}).get("trench"): + return SECTION_TRENCH_NOTE return "⚠ 터파기·되메우기·잔토가 안 섬 — 관측 원단위 표에 그 줄이 없음(값을 지어내지 않음)" -def weep_option_note(entry: dict[str, Any], options: dict[str, Any]) -> str | None: - """제원의 물빼기 칸을 채웠는데 관측 값이 붙박이라 **표에 안 닿을 때** 한 줄(표·그림 같은 말).""" - weep = (entry.get("section") or {}).get("weep") - keys = ("weep_hole_diameter_mm", "weep_hole_area_m2") - if not weep or all(options.get(key) in (None, "") for key in keys): +def weep_component( + entry: dict[str, Any], options: dict[str, Any], scale: float, scale_note: str +) -> dict[str, Any] | None: + """물구멍관 — 단면이 있는 관측 줄은 **실무 식에 제원 칸을 넣어** 다시 셈(붙박이 값 대신). + + 식 `벽 높이 ÷ 개소당 벽면적 × 관 길이`(소광리 「옹벽2.0」 T31). 관 길이는 실무 관측값이라 + `basis_kind` 는 그대로 `observed`. 규격 `Ø지름` 을 달아 돌쌓기 물구멍관과 (이름+규격)으로 합침. + """ + from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import weep_hole_spec + + section = entry.get("section") or {} + weep = section.get("weep") + if not weep: return None - return ( - f"⚠ 물빼기 지름·면적 칸이 이 표에 안 닿음 — 관측 원단위 붙박이(Ø{weep['diameter_mm']}" - f" · {weep['area_m2']:g}㎡당 1개소 · 관 {weep['pipe_length_m']:g} m)" + area, diameter, spec_basis = weep_hole_spec(options) + height, length = section["wall"]["height_m"], weep["pipe_length_m"] + per_unit = height / area * length + return { + "name": "물구멍관", + "unit": "m", + "amount": per_unit * scale, + "destination": "material", + "basis": ( + f"관측 원단위 식(소광리 「옹벽2.0」 T31) 벽 높이 {height:g} ÷ {area:g}㎡/개소" + f" × 관 {length:g} m = {per_unit:.3f}/{entry.get('unit')} × {scale_note}" + f" · 관 길이는 관측값 · {spec_basis}" + ), + "basis_kind": BASIS_OBSERVED, + "source": str(entry.get("source") or ""), + "spec": f"Ø{diameter}", + } + + +def section_trench_components( + entry: dict[str, Any], rubble_thickness_m: float, scale: float +) -> tuple[list[dict[str, Any]], float] | None: + """단면이 있는 관측 줄의 터파기·되메우기·잔토(`destination: earthwork`)와 파는 깊이(m). + + ⚠ 브레인 판정 — 기초 폭 + 양쪽 여유(0.3, 같은 파일 식생옹벽블럭 기초 터파기)로 **수직** · + 깊이 = 기초 + 버림 + 기초잡석 · **비탈분(벽 높이 몫)은 안 셈**(지반선이 제원에 없음). + ⚠ 되메우기 = 터파기 − 그 안에 든 것(기초·전단키 콘크리트 + 버림 + 기초잡석) — 잔토 = 든 것. + """ + section = entry.get("section") or {} + trench = section.get("trench") + if not trench: + return None + footing, key, blinding = section["footing"], section["key"], section["blinding"] + clearance = trench["clearance_m"] + width = footing["width_m"] + 2 * clearance + depth = footing["thickness_m"] + blinding["thickness_m"] + rubble_thickness_m + key_below = max(key["depth_m"] - blinding["thickness_m"] - rubble_thickness_m, 0.0) + excavation = width * depth + key["width_m"] * key_below + layer_width = footing["width_m"] + 2 * blinding["overhang_m"] - key["width_m"] + filled = ( + footing["width_m"] * footing["thickness_m"] + + key["width_m"] * key["depth_m"] + + layer_width * (blinding["thickness_m"] + rubble_thickness_m) ) + backfill = excavation - filled + basis = ( + f"(기초 폭 {footing['width_m']:g} + 여유 {clearance:g}×2) × 깊이 {depth:.2f}" + f"(기초 {footing['thickness_m']:g} + 버림 {blinding['thickness_m']:g}" + f" + 기초잡석 {rubble_thickness_m:g})" + f" · {SECTION_TRENCH_NOTE} · 여유는 소광리 식생옹벽블럭 기초 터파기" + ) + rows = [ + ("터파기", excavation, basis), + ("되메우기", backfill, f"터파기 − 든 것 {filled:.3f}(기초·전단키 + 버림 + 기초잡석)"), + ("잔토처리", filled, "터파기 − 되메우기"), + ] + return [ + { + "name": name, + "unit": "㎥", + "amount": amount * scale, + "destination": "earthwork", + "basis": f"{text} = {amount:.3f}/{entry.get('unit')}", + "basis_kind": BASIS_DERIVED, + "source": str(entry.get("source") or ""), + } + for name, amount, text in rows + ], depth def expand_observed( @@ -192,9 +270,18 @@ def expand_observed( return [], [f"{type_label(type_id)}의 연장·면적이 0 이라 물량을 내지 않았습니다"] source_key = str(found.get("source") or "") + options = structure.get("options") or {} components: list[dict[str, Any]] = [] for item in found.get("components") or []: note = str(item.get("basis_note") or "") + weep = ( + weep_component(found, options, scale, scale_note) + if item["name"] == "물구멍관" + else None + ) + if weep is not None: + components.append(weep) + continue components.append( { "name": item["name"], @@ -209,10 +296,7 @@ def expand_observed( } ) notes = [f"관측 원단위 적용 — {found.get('source_note') or source_key}"] - for note in ( - earthwork_missing_note(found), - weep_option_note(found, structure.get("options") or {}), - ): - if note: - notes.append(note) + earthwork_note = earthwork_missing_note(found) + if earthwork_note: + notes.append(earthwork_note) return components, notes diff --git a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py index a3ab3078..d2efd115 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py @@ -236,10 +236,7 @@ def _wall_figure( x 는 앞굽 끝이 0(앞면 왼쪽), y 는 기초 밑면이 0. 버림·기초잡석은 기초 밑, 전단키가 뚫고 내려감. ⚠ 기초잡석 두께는 표의 기초잡석 줄과 같은 산출 조건 값(비면 기본) — 폭은 버림과 같음. """ - from B08_Quantity.B08_Quantity_Engine_ObservedUnit import ( - earthwork_missing_note, - weep_option_note, - ) + from B08_Quantity.B08_Quantity_Engine_ObservedUnit import earthwork_missing_note from B08_Quantity.B08_Quantity_Engine_UnitQuantity import RUBBLE_BASE_THICKNESS_M section = entry["section"] @@ -297,8 +294,26 @@ def _wall_figure( shapes.append( _path([(toe + front * ratio, weep_y), (toe + w_bottom - back * ratio, weep_y)], "guide") ) + # 터파기 — 표 줄과 같은 식(기초 폭 + 양쪽 여유 · 수직 · 기초 윗면에서 기초잡석 밑까지). 점선. + trench = section.get("trench") + clearance = trench["clearance_m"] if trench else 0.0 + dig_bottom = -(b_t + rubble) + if trench: + shapes.append( + _path( + [ + (-clearance, f_t), + (-clearance, dig_bottom), + (f_w + clearance, dig_bottom), + (f_w + clearance, f_t), + ], + "guide", + dash=True, + ) + ) - right = f_w + over + 0.2 + right = max(f_w + over, f_w + clearance) + 0.2 + weep_area, weep_dia, _weep_basis = weep_hole_spec(sheet.get("options") or {}) blinding_width = f_w + 2 * over - k_w shapes += [ _text( @@ -316,8 +331,8 @@ def _wall_figure( "left", ), _text( - f"물구멍 Ø{weep['diameter_mm']} — {weep['area_m2']:g}㎡당 1개소" - f" · 관 {weep['pipe_length_m']:g} m", + f"물구멍 Ø{weep_dia} — {weep_area:g}㎡당 1개소" + f" · 관 {weep['pipe_length_m']:g} m(관측값)", (right, weep_y), "left", ), @@ -341,11 +356,19 @@ def _wall_figure( "left", ) ) + if trench: + shapes.append( + _text( + f"터파기 폭 {f_w + 2 * clearance:.2f} × 깊이 {f_t - dig_bottom:.2f} m" + f" (기초 {f_w:g} + 여유 {clearance:g}×2)", + (right, f_t + 0.25), + "left", + ) + ) below = -max(k_d, b_t + rubble) - 0.36 notes = [ f"단면 — 관측 원단위 {entry.get('source_note') or ''}의 수량이 나온 그 단면", earthwork_missing_note(entry), - weep_option_note(entry, sheet.get("options") or {}), ] for index, note in enumerate(note for note in notes if note): shapes.append(_text(note, (0.0, below - index * 0.16), "left")) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index f518b782..26072a38 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -448,6 +448,33 @@ def _rubble_base_component( ) +def _append_section_trench( + quantity: StructureQuantity, + structure: dict[str, Any], + observed: ObservedUnitTable, + thickness_m: float | None, +) -> None: + """단면이 실린 관측 줄(옹벽)의 터파기·되메우기·잔토 — 기초잡석과 **같은 두께**로 깊이를 잡음.""" + from B08_Quantity.B08_Quantity_Engine_ObservedUnit import scale_for, section_trench_components + + keys = OBSERVED_SPEC_KEYS.get(quantity.type_id) + if not keys or not quantity.components: + return + options = structure.get("options") or {} + entry = observed.find( + quantity.type_id, {key: options[key] for key in keys if options.get(key) is not None} + ) + if entry is None: + return + scale, _note = scale_for(entry, structure) + thickness = RUBBLE_BASE_THICKNESS_M if thickness_m is None else float(thickness_m) + found = section_trench_components(entry, max(thickness, 0.0), scale) + if found is None: + return + rows, quantity.trench_depth_m = found + quantity.components.extend(Component(**row) for row in rows) + + def build_table( structures: Iterable[dict[str, Any]], names: dict[str, str] | None = None, @@ -482,6 +509,7 @@ def build_table( rubble = _rubble_base_component(quantity.components, rubble_base_thickness_m) if rubble is not None: quantity.components.append(rubble) + _append_section_trench(quantity, item, observed, rubble_base_thickness_m) quantities.append(quantity) if use_templates: # 늦게 부름 — 양식 모듈이 이 모듈을 부르므로 맨 위에서 부르면 맞물림. @@ -531,6 +559,8 @@ def build_table( # ⚠ 여기서 새로 만드는 값이 아니라 측점 설계값(`design.ground_type`)을 옮긴 것이다. "ground_type": item.ground_type, "ground_type_basis": item.ground_type_basis, + # 파는 깊이 — 단면으로 터파기를 세운 종류만(옹벽 기초분). 비면 직고 + 기초 깊이. + "trench_depth_m": item.trench_depth_m, # 양식 있음/없음 — 화면이 가림(비면 지금 전개). "library_item": item.library_item, "notes": item.notes, diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index 7e3d4ef8..c57cd173 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -104,6 +104,8 @@ class StructureQuantity: #: 품셈 9-13 구조물터파기의 **토질 축**이 이 값으로 갈린다. 못 가르면 `None` 이다. ground_type: str | None = None ground_type_basis: str = "" + #: 파는 깊이(m) — 단면으로 터파기를 세운 종류만(옹벽 기초분). 비면 인계가 직고 + 기초 깊이로 봄. + trench_depth_m: float | None = None #: 양식으로 성분을 세웠으면 그 양식 이름(PLAN 3장 ④-2). 비면 지금 전개 값. library_item: str = "" diff --git a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json index d5d26701..cf98db0e 100644 --- a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json +++ b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json @@ -34,33 +34,41 @@ "source": "uljin_library", "source_note": "§7 옹벽류 — 반중력식옹벽 H=2.0", "section": { - "note": "단면 치수(m) — 위 수량이 나온 그 단면. 출처 소광리 07-구조도 「옹벽2.0」 탭 산출식(T22 기초 0.4×1.6+0.3×0.35 · T23 벽체 (0.3+0.45)÷2×1.6 · T25 버림 0.1×(1.8−0.35) · T26·T27 유로폼 √(1.6²+0.032²)+√(1.6²+0.118²) · T31 파이프 1.6÷2×0.4)과 같은 탭 일반도(앞굽 950·뒷굽 200·키 자리 뒷굽 끝에서 500). 원문 1.345·0.145·3.205 를 라이브러리가 1.35·0.15·3.2 로 적음", + "note": "단면 치수(m) — 아래 수량이 나온 그 단면. 출처 소광리 07-구조도 「옹벽2.0」 탭 산출식(T22 기초 0.4×1.6+0.3×0.35 · T23 벽체 (0.3+0.45)÷2×1.6 · T25 버림 0.1×(1.8−0.35) · T26·T27 유로폼 √(1.6²+0.032²)+√(1.6²+0.118²) · T31 파이프 1.6÷2×0.4)과 같은 탭 일반도(앞굽 950·뒷굽 200·키 자리 뒷굽 끝에서 500)", "footing": { "width_m": 1.6, "thickness_m": 0.4, "toe_m": 0.95 }, "wall": { "height_m": 1.6, "top_m": 0.3, "front_batter_m": 0.118, "back_batter_m": 0.032 }, "key": { "width_m": 0.35, "depth_m": 0.3, "from_heel_m": 0.5 }, "blinding": { "thickness_m": 0.1, "overhang_m": 0.1 }, - "weep": { "area_m2": 2.0, "diameter_mm": 50, "pipe_length_m": 0.4 } + "weep": { + "pipe_length_m": 0.4, + "note": "소광리 「옹벽2.0」 T31 「파이프 φ50 (1.6 ÷ 2) × 0.4」 — 벽 높이 ÷ 개소당 벽면적 × 관 길이. 관 길이 0.4 는 **실무 관측값**(품셈에 없음) · 개소당 벽면적·지름은 제원 칸(비우면 2㎡ · Ø50)" + }, + "trench": { + "clearance_m": 0.3, + "note": "소광리 「옹벽2.0」 탭에 터파기 줄이 없음 → 같은 파일 「식생옹벽블럭(H=2.0)」 F31 기초 터파기 밑폭 = 기초 폭 + 0.6(양쪽 0.3). 브레인 판정(2026-09-14) — 수직 · 비탈분 제외(지반선이 제원에 없음)" + } }, "components": [ { "name": "콘크리트", "unit": "㎥", - "amount": 1.35, + "amount": 1.345, "destination": "unit_price", - "basis_note": "기초 0.75 + 벽체 0.60" + "basis_note": "기초 0.745 + 벽체 0.60 · 원문 값(라이브러리 표기 1.35)" }, { "name": "버림콘크리트", "unit": "㎥", - "amount": 0.15, - "destination": "unit_price" + "amount": 0.145, + "destination": "unit_price", + "basis_note": "원문 값(라이브러리 표기 0.15)" }, { "name": "유로폼", "unit": "㎡", - "amount": 3.2, + "amount": 3.205, "destination": "unit_price", - "basis_note": "배면+전면" + "basis_note": "배면+전면 · 원문 값(라이브러리 표기 3.20)" }, { "name": "합판거푸집", diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index 6c3bf199..86ceb393 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -345,7 +345,7 @@ "from_components": [ "기초잡석" ], - "why": "관측 원단위에는 기초잡석 줄이 없으나 **버림 폭이 곧 잡석다짐 폭**이라(KCS 34 50 05) 버림 물량에서 두께 비로 나온다 — 버림 0.15㎥/m ÷ 0.1 = 폭 1.5m, 두께 0.2m(사용자 확정 3차 ②) ⇒ 0.30㎥/m." + "why": "관측 원단위에는 기초잡석 줄이 없으나 **버림 폭이 곧 잡석다짐 폭**이라(KCS 34 50 05) 버림 물량에서 두께 비로 나온다 — 버림 0.145㎥/m ÷ 0.1 = 폭 1.45m(1.8 − 전단키 0.35), 두께 0.2m(사용자 확정 3차 ②) ⇒ 0.29㎥/m(원문 값 · 2026-09-14 라이브러리 반올림 0.15 에서 바꿈)." } ], "why": "품셈 12장에 「옹벽」 공종이 없음. 실무 내역은 「반중력식옹벽 H=2.0」 한 줄이고 그 일위대가가 위 공종을 묶음.", diff --git a/resources/tester/test_b08_formwork.py b/resources/tester/test_b08_formwork.py index 14a5879f..52d7cb82 100644 --- a/resources/tester/test_b08_formwork.py +++ b/resources/tester/test_b08_formwork.py @@ -51,7 +51,8 @@ def test_횟수별_비율을_여기서_곱하지_않을것() -> None: euroform = next( c for s in table["structures"] for c in s["components"] if c["name"] == "유로폼" ) - assert euroform["amount"] == 32.0 # 3.20 ㎡/m × 10m — 46.1 % 를 곱하지 않았다 + # 3.205 ㎡/m × 10m — 46.1 % 를 곱하지 않았다(2026-09-14 원문 값 3.205 · 전 라이브러리 3.20 → 32.0) + assert abs(euroform["amount"] - 32.05) < 1e-9 # 비율표는 데이터에 있되 값에 안 걸린다. assert load_formwork_table().reuse_ratio_pct["plywood"]["3"] == 46.1 diff --git a/resources/tester/test_b08_handoff.py b/resources/tester/test_b08_handoff.py index 173643d5..9e95f95f 100644 --- a/resources/tester/test_b08_handoff.py +++ b/resources/tester/test_b08_handoff.py @@ -791,11 +791,10 @@ def 조각(row: dict, code_fragment: str) -> dict: def test_조각마다_수량과_단위가_실릴것() -> None: """코드만 보내면 받는 쪽이 상세 줄을 못 세운다.""" row = build_handoff(unit_quantity_table=옹벽단위())["work_items"][0] - assert 조각(row, "12-01-01")["quantity"] == pytest.approx( - 15.0 - ) # 콘크리트 1.35 + 버림 0.15, ×10m + # 콘크리트 1.345 + 버림 0.145, ×10m — 2026-09-14 원문 값(전 라이브러리 1.35 + 0.15 → 15.0) + assert 조각(row, "12-01-01")["quantity"] == pytest.approx(14.9) assert 조각(row, "12-04")["quantity"] == pytest.approx(6.0) - assert 조각(row, "12-38")["quantity"] == pytest.approx(32.0) + assert 조각(row, "12-38")["quantity"] == pytest.approx(32.05) def test_철근은_ton_으로_환산될것() -> None: @@ -862,12 +861,12 @@ def test_기초잡석은_버림_폭에서_선다() -> None: 그 조각이 **채워졌다**(2026-09-09 확정 3차 ②). 버림 폭이 곧 잡석다짐 폭이라 (KCS 34 50 05) 두께 비만 곱하면 관측 원단위 구조물에도 값이 선다 — - 버림 1.5㎥ × (0.2 ÷ 0.1) = 3.0㎥(= 0.30㎥/m × 10m). + 버림 1.45㎥ × (0.2 ÷ 0.1) = 2.9㎥(= 0.29㎥/m × 10m · 2026-09-14 원문 버림 0.145, 전 3.0). 「못 채운 조각은 0 이 아니라 사유와 함께」라는 규칙 자체는 다른 조각 시험이 지킨다. """ row = build_handoff(unit_quantity_table=옹벽단위())["work_items"][0] gravel = 조각(row, "12-25") - assert gravel["quantity"] == 3.0 + assert gravel["quantity"] == pytest.approx(2.9) assert not gravel.get("not_ready") diff --git a/resources/tester/test_b08_observed_unit.py b/resources/tester/test_b08_observed_unit.py index 14d29b21..ee911672 100644 --- a/resources/tester/test_b08_observed_unit.py +++ b/resources/tester/test_b08_observed_unit.py @@ -51,8 +51,9 @@ def 성분(result, name: str): def test_규격이_맞으면_연장만큼_곱해짐() -> None: """`H=2.0 옹벽 10m` 는 같은 단면이 10m 이어진 것 — 개수를 세는 것이지 규격을 늘리는 게 아니다.""" result = expand(옹벽(length=10.0)) - assert 성분(result, "콘크리트").amount == pytest.approx(13.5) # 1.35 ㎥/m × 10 - assert 성분(result, "유로폼").amount == pytest.approx(32.0) + # 원문 값 1.345 ㎥/m × 10 · 유로폼 3.205(2026-09-14 · 전 라이브러리 1.35 → 13.5 · 3.20 → 32.0) + assert 성분(result, "콘크리트").amount == pytest.approx(13.45) + assert 성분(result, "유로폼").amount == pytest.approx(32.05) def test_높이가_다르면_비례로_늘리지_않고_미확보() -> None: @@ -114,8 +115,11 @@ def test_표에도_근거가_실림() -> None: 관측이 아니라 `derived` 다(2026-09-09 확정 3차 ②). 근거가 갈려 실리는 것이 맞다.""" table = build_table([옹벽()]) components = [c for s in table["structures"] for c in s["components"]] - kinds = {c["basis_kind"] for c in components if c["name"] != "기초잡석"} + # 2026-09-14 — 단면으로 세운 터파기·되메우기·잔토도 파생 줄(브레인 판정 ①). + derived = {"기초잡석", "터파기", "되메우기", "잔토처리"} + kinds = {c["basis_kind"] for c in components if c["name"] not in derived} assert kinds == {BASIS_OBSERVED} + assert {c["basis_kind"] for c in components if c["name"] in derived} == {BASIS_DERIVED} 잡석 = next(c for c in components if c["name"] == "기초잡석") assert 잡석["basis_kind"] == BASIS_DERIVED diff --git a/resources/tester/test_b08_rubble_base.py b/resources/tester/test_b08_rubble_base.py index b5e9fcfa..eb31e5b9 100644 --- a/resources/tester/test_b08_rubble_base.py +++ b/resources/tester/test_b08_rubble_base.py @@ -3,7 +3,8 @@ ⚠ 겨누는 것 여섯 ① 두께는 **0.2 m**(사용자 확정) — 품셈 12-25 는 ㎥당 품만 주고 두께를 안 정함 ② 폭을 다시 세지 않는다 — **버림 폭이 곧 잡석다짐 폭**(KCS 34 50 05)이라 두께 비만 곱함 - ③ ⚠ 그래서 **관측 원단위로 오는 옹벽에도 값이 선다**(버림 0.15㎥/m ⇒ 잡석 0.30㎥/m) + ③ ⚠ 그래서 **관측 원단위로 오는 옹벽에도 값이 선다**(버림 0.145㎥/m ⇒ 잡석 0.29㎥/m + · 2026-09-14 원문 값 — 전 라이브러리 반올림 0.15 ⇒ 0.30) ④ 두께는 화면에서 바꿀 수 있다 ⑤ ⚠ 자재총괄에 안 섞인다 — 운반·부설·다짐 품이 붙는 **공종**이다 ⑥ ⚠ 묶음으로 서는 구조물은 줄을 또 세우지 않는다(옹벽 조각이 이미 셈) @@ -86,7 +87,7 @@ def test_버림_두께_비로_나온다() -> None: def test_관측_원단위_구조물에도_선다() -> None: """⚠ 옹벽은 관측 원단위로 오는데 그 표에 기초잡석 줄이 없다 — 버림에서 나온다.""" - assert abs(성분(옹벽(), "기초잡석")["amount"] - 3.0) < 1e-9 # 0.30㎥/m × 10m + assert abs(성분(옹벽(), "기초잡석")["amount"] - 2.9) < 1e-9 # 0.29㎥/m × 10m def test_두께를_바꾸면_따라간다() -> None: @@ -112,4 +113,4 @@ def test_묶음_구조물은_줄을_또_세우지_않는다() -> None: assert all(row["name"] != "기초잡석" for row in rows) parts = rows[0]["composite_parts"] gravel = next(part for part in parts if part["code"] == "FP-12-25") - assert gravel["quantity"] == 3.0 + assert abs(gravel["quantity"] - 2.9) < 1e-9 diff --git a/resources/tester/test_b08_structure_figure.py b/resources/tester/test_b08_structure_figure.py index e8589556..ffac3218 100644 --- a/resources/tester/test_b08_structure_figure.py +++ b/resources/tester/test_b08_structure_figure.py @@ -130,15 +130,58 @@ def test_옹벽_단면_치수가_표의_관측_수량을_그대로_낸다() -> N form = math.hypot(wall["height_m"], wall["back_batter_m"]) + math.hypot( wall["height_m"], wall["front_batter_m"] ) - weep = section["weep"] - pipe = wall["height_m"] / weep["area_m2"] * weep["pipe_length_m"] - assert round(concrete, 2) == amounts["콘크리트"] # 1.345 - assert round(blind, 2) == amounts["버림콘크리트"] # 0.145 - assert round(form, 1) == amounts["유로폼"] # 3.205 - assert round(pipe, 2) == amounts["물구멍관"] + pipe = wall["height_m"] / 2.0 * section["weep"]["pipe_length_m"] + # 원문 값 그대로(2026-09-14 브레인 판정 — 라이브러리 반올림 1.35·0.15·3.2 에서 바꿈) + assert abs(concrete - amounts["콘크리트"]) < 1e-9 # 1.345 + assert abs(blind - amounts["버림콘크리트"]) < 1e-9 # 0.145 + assert abs(form - amounts["유로폼"]) < 5e-4 # 3.2047 → 3.205 + assert abs(pipe - amounts["물구멍관"]) < 1e-9 # 0.32 (제원 칸 비었을 때) assert footing["thickness_m"] + wall["height_m"] == 2.0 +def _wall_table(length: float = 10.0, rubble=None, **options) -> dict: + from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table + + structure = { + "structure_id": "w1", + "type_id": "retaining_wall", + "start_m": 0.0, + "end_m": length, + "options": {"form": "반중력식", "height_m": 2.0, "length_m": length, **options}, + } + table = build_table([structure], {"retaining_wall": "옹벽"}, rubble_base_thickness_m=rubble) + return table["structures"][0] + + +def _amounts(structure: dict) -> dict: + return {row["name"]: row for row in structure["components"]} + + +def test_옹벽_터파기는_기초_폭과_양쪽_여유로_수직이고_토공으로만_간다() -> None: + wall = _wall_table() + rows = _amounts(wall) + # 폭 1.6 + 0.3×2 = 2.2 · 깊이 기초 0.4 + 버림 0.1 + 기초잡석 0.2 = 0.7 → 1.54/m + assert abs(rows["터파기"]["amount"] - 15.4) < 1e-9 + # 든 것 = 기초·전단키 0.745 + (버림 + 잡석) 1.45 × 0.3 = 1.18 → 되메우기 0.36/m + assert abs(rows["잔토처리"]["amount"] - 11.8) < 1e-9 + assert abs(rows["되메우기"]["amount"] - 3.6) < 1e-9 + assert {rows[k]["destination"] for k in ("터파기", "되메우기", "잔토처리")} == {"earthwork"} + assert abs(rows["기초잡석"]["amount"] - 2.9) < 1e-9 # 원문 버림 0.145 × 2 + assert wall["trench_depth_m"] == 0.7 + assert "비탈분 제외" in rows["터파기"]["basis"] + # 잡석을 얇게 두면 전단키가 그 밑으로 나와 그 몫을 더 팜: 2.2×0.6 + 0.35×0.1 + thin = _amounts(_wall_table(rubble=0.1)) + assert abs(thin["터파기"]["amount"] - 13.55) < 1e-9 + + +def test_옹벽_물구멍관은_제원_칸이_닿고_돌쌓기와_같은_규격으로_합쳐진다() -> None: + plain = _amounts(_wall_table())["물구멍관"] + assert plain["spec"] == "Ø50" and abs(plain["amount"] - 3.2) < 1e-9 + assert plain["basis_kind"] == "observed" and "관측값" in plain["basis"] + given = _amounts(_wall_table(weep_hole_area_m2=2.5, weep_hole_diameter_mm=75))["물구멍관"] + assert given["spec"] == "Ø75" and abs(given["amount"] - 1.6 / 2.5 * 0.4 * 10) < 1e-9 + + def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사유는_표와_같다() -> None: shapes = build_figure(_wall_sheet(), rubble_thickness_m=0.3) texts = _texts(shapes) @@ -146,14 +189,15 @@ def test_옹벽_그림은_기초잡석_두께를_산출_조건에서_받고_사 assert "버림 T=0.10 · 폭 1.80 − 키 0.35 = 1.45 m" in texts wall = _paths(shapes)[0]["points"] assert [0.95, 0.4] in wall and [1.068, 2.0] in wall and [1.368, 2.0] in wall - # 표 사유와 같은 말 — 터파기 줄이 없다는 것 + # 표 사유와 같은 말 — 터파기를 어떻게 셌나(비탈분 제외) _rows, table_notes = expand_observed( "retaining_wall", {"form": "반중력식", "height_m": 2.0}, _wall_sheet() | {"options": {"length_m": 10}}, ) - missing = [note for note in table_notes if "터파기" in note] - assert missing and missing[0] in texts + trench = [note for note in table_notes if "터파기" in note] + assert trench and trench[0] in texts and "비탈분 제외" in trench[0] + assert "터파기 폭 2.20 × 깊이 0.80 m (기초 1.6 + 여유 0.3×2)" in texts # 잡석 0.3 assert not any(text.startswith("기초잡석") for text in _texts(build_figure(_wall_sheet(), 0))) @@ -161,8 +205,8 @@ def test_옹벽_못_그리는_장은_표와_같은_까닭() -> None: assert "형식" in figure_reason({**_wall_sheet(), "options": {"height_m": 2.0}}) other = figure_reason(_wall_sheet(height_m=1.6) | {"height_m": 1.6}) assert other.startswith("그림 없음 — ") and "자료에 없습니다" in other - note = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "안 닿음" in t] - assert note and "Ø50" in note[0] + label = [t for t in _texts(build_figure(_wall_sheet(weep_hole_area_m2=3))) if "물구멍" in t] + assert label == ["물구멍 Ø50 — 3㎡당 1개소 · 관 0.4 m(관측값)"] def test_양식_장은_양식이_푼_뒷길이로_그린다() -> None: