From 1395ae6d34d03ef1dfd6f4a6de7c613740c182dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 15 Sep 2026 06:49:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(b05=C2=B7b08):=20=EC=84=A0=EB=96=BC?= =?UTF-8?q?=EB=B6=99=EC=9D=B4=EA=B8=B0=20=E2=80=94=20=EC=97=B0=EC=9E=A5=20?= =?UTF-8?q?=3D=20=EC=82=AC=EB=A9=B4=20=EB=82=99=EC=B0=A8=20=C3=97=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=AC=20=C3=B7=20=EB=8B=A8=20=EC=A7=81=EA=B3=A0(?= =?UTF-8?q?=ED=92=88=EC=85=88=205-16-1=20[=EC=A3=BC]=E2=91=A1)=20=C2=B7=20?= =?UTF-8?q?=EB=96=BC=20=EB=A7=A4=EC=88=98=20=3D=20=EC=82=AC=EB=B0=A9?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=EA=B5=90=EB=B3=B8=20=ED=91=9C=202-2-6=20?= =?UTF-8?q?=ED=91=9C=20=EA=B0=92(=EC=96=B4=EA=B8=8B=EB=82=A8=20=EB=91=98?= =?UTF-8?q?=20=EC=82=AC=EC=9C=A0)=20=C2=B7=20=EB=96=BC=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=C2=B7=EB=8B=A8=EB=81=8A=EA=B8=B0=20=ED=86=A0=EC=A7=88?= =?UTF-8?q?=20=EC=B9=B8=20=EA=B8=B0=EB=B3=B8=20=EC=97=86=EC=9D=8C=20=C2=B7?= =?UTF-8?q?=20=EB=96=BC=EB=B6=99=EC=9E=84=20=E3=8E=A1=20=EB=8A=94=20?= =?UTF-8?q?=ED=98=B8=ED=91=9C=20=EC=97=86=EC=96=B4=20=EB=A7=89=ED=9E=98=20?= =?UTF-8?q?+=20=EC=82=AC=EC=9C=A0(=EB=B8=8C=EB=A0=88=EC=9D=B8=20=E2=93=90~?= =?UTF-8?q?=E2=93=94)=20=C2=B7=20=EB=85=B9=ED=99=94=20=EB=B3=91=ED=96=89?= =?UTF-8?q?=EC=8B=9C=EA=B3=B5=20=EC=82=AC=EC=9C=A0=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Structure_Types.json | 26 ++++- .../B08_Quantity_Engine_Handoff_Mapping.py | 11 ++ .../B08_Quantity_Engine_UnitQuantity_Base.py | 3 + ...antity_Engine_UnitQuantity_Revegetation.py | 103 ++++++++++++++++-- .../work_item_mapping_2026-01-01.json | 23 ++++ .../tester/test_b05_not_in_table_marks.py | 5 - .../tester/test_b08_revegetation_unit.py | 90 ++++++++++++++- 7 files changed, 237 insertions(+), 24 deletions(-) diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index fd87b6a9..5353df62 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -2519,8 +2519,7 @@ "default": null, "required": false, "phase": "detail", - "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음", - "not_in_table": "아직 표에 안 쓰임 — 길이형(선떼·조공) 산출식이 아직 없음(선떼는 사방기술교본 표 2-2-6 으로 다음 차례)" + "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음 · 비면 선떼 연장(품셈 5-16-1 [주]② 대상지 직고 ÷ 단 직고 × 연장)이 안 섬" }, { "key": "sod_grade", @@ -2530,8 +2529,27 @@ "default": null, "required": false, "phase": "detail", - "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음", - "not_in_table": "아직 표에 안 쓰임 — 선떼붙이기 산출식이 아직 없음(사방기술교본 표 2-2-6 급별 m당 매수로 다음 차례)" + "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음 · 비면 m당 떼 매수(사방기술교본 표 2-2-6)가 안 섬" + }, + { + "key": "sod_size", + "label": "선떼 떼 크기", + "input": "select", + "choices": ["대형 40×40", "소형 18×18"], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "사방기술교본 표 2-2-6 이 대형(40×40)·소형(18×18) 두 줄을 나란히 줌 — 우리가 고를 자리가 아니라 기본값 없음 · 비면 선떼 떼 매수가 안 섬" + }, + { + "key": "cutting_soil", + "label": "단끊기 토질", + "input": "select", + "choices": ["보통토사", "경질ㆍ고사점토 및 자갈섞인 점토", "호박돌 섞인 토사"], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "품셈 5-16-1 단끊기 표의 토질 셋 — B06 지반(토사/암)과 다른 축이라 자동으로 안 고름 · 비면 단끊기 조각만 막힘" } ] }, diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py index 79c2693d..b61ab755 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py @@ -431,6 +431,17 @@ def composite_quantities( entry["not_ready"] = True entry["why"] = why missing.append({"code": spec.get("code"), "reason": why}) + if suffix == "cutting_soil": + # 단끊기 5-16-1 토질 셋 — 칸 값 그대로(B06 지반과 다른 축 · 2026-09-15 브레인 ⓑ). + soil = str((structure.get("options") or {}).get("cutting_soil") or "") + if soil: + entry["kind"] = normalize_kind_key(soil) + entry["code"] = f"{spec.get('code')}#{normalize_kind_key(soil)}" + elif found: + why = "단끊기 토질을 안 고름 — 품셈 5-16-1 표의 토질 셋 중 고를 것(기본값 없음)" + entry["not_ready"] = True + entry["why"] = why + missing.append({"code": spec.get("code"), "reason": why}) if suffix == "rebar_complexity": # 갈래는 원문이 정한다 — 화면·인계에 이름과 근거를 함께 실어 사람이 검증하게 한다. complexity, why = rebar_complexity( diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index d15677b5..2373943b 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -72,6 +72,9 @@ DESTINATION = { "평떼": "unit_price", "줄떼": "unit_price", "비탈덮기": "unit_price", + # 선떼붙이기(5-16) — 단끊기 m · 떼붙임 ㎡ 은 품 · 떼는 위 「떼」(자재). + "선떼붙이기": "unit_price", + "떼붙임": "unit_price", } #: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄. diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py index ec05911e..7334720b 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py @@ -6,7 +6,9 @@ 5-25 거적덮기(㎡당) · 5-28 비탈덮기(5-28-1 짚망 · 5-28-2 방초·야자섬유매트) 교본 7-1 그림 7-8~7-12 는 사진·개념도(치수 없음) — 표준도가 아님. ⭐ 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽). -⚠ 길이형(선떼·조공)은 다음 차례(선떼 = 사방기술교본 표 2-2-6 급별 m당 매수) · 나머지 공법은 사유. +⭐ 선떼붙이기(길이형) — 연장 = 대상지 직고 ÷ 단 직고 × 연장(품셈 5-16-1 [주]②) · m당 떼 매수 = + 사방기술교본 표 2-2-6 **표 값**(단면식은 역산 · 어긋나면 표가 이김 · 2026-09-15 브레인 ⓔ). +⚠ 조공은 다음 차례 · 나머지 공법은 사유. """ from __future__ import annotations @@ -33,12 +35,27 @@ AREA_METHODS: dict[str, tuple[str, float | None, str]] = { ), } SOD_SPEC = "0.3×0.3" -LENGTH_METHODS = ("선떼붙이기", "조공") -NOTE_LENGTH = ( - "{method}은(는) 길이형(10m·100m당) — 산출식 아직 없음 · 떼 매수는 품셈이" - " 「사방기술교본 기준표」라 함(선떼 = 표 2-2-6 급별 m당 매수 · 다음 차례)" - " · 단 직고·급수 칸은 기본값 없음(브레인 ⓒ)" +LENGTH_METHODS = ("조공",) +NOTE_LENGTH = "{method}은(는) 길이형(100m·m당) — 산출식 아직 없음(선떼붙이기 다음 차례)" +CUT_RISE, FILL_RISE = "b06_cut_rise_area_m2", "b06_fill_rise_area_m2" +RISES = {"절토면": (CUT_RISE,), "성토면": (FILL_RISE,), "양쪽": (CUT_RISE, FILL_RISE)} +#: 사방기술교본 표 2-2-6 「각 급별 선떼붙이기 m당 떼사용 매수표」 — 1m당 **표 값** · 한 장 ㎡. +SOD_TABLE: dict[str, tuple[str, float, tuple[float, ...]]] = { + "대형 40×40": ("40×40", 0.16, (12.50, 11.25, 10.00, 8.75, 7.80, 6.25, 5.00, 3.75, 2.50)), + "소형 18×18": ("18×18", 0.0324, (30.0, 27.0, 24.0, 21.0, 18.0, 15.0, 12.0, 9.0, 6.0)), +} +NOTE_SOD = ( + "선떼붙이기 — 연장 = 대상지 직고(B06 사면 낙차) ÷ 단 직고 × 연장(품셈 5-16-1 [주]②)" + " · m당 떼 매수 = 사방기술교본 표 2-2-6 표 값(식은 역산) · 떼붙임 ㎡ = 매수 × 떼 한 장 면적" ) +NOTE_SOD_TABLE_GAP = ( + "⚠ 표 2-2-6 안 어긋남 — 5급 대형 표 7.80 ↔ 단면식 7.50 · 소형 2급 표 27.0 ↔ 단면 4.0" + "(×6 = 24.0) · 표 값을 씀(표가 고쳐지면 그때 봄)" +) +NOTE_SOD_HAUL = ( + "떼운반·지게운반(5-16-2 표 「물량산출」)은 운반거리(200m 조견표)가 설계 입력이라 안 섬" +) +NOTE_SOD_MISSING = "선떼붙이기 {what} 칸이 비어 연장·떼 매수가 안 섬 — 칸에 적을 것(기본 없음)" NOTE_LATER = ( "{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)" ) @@ -47,8 +64,8 @@ NOTE_COVER = ( "비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것" ) NOTE_OVERLAP = ( - "⚠ 같은 사면이 초류종자살포·면고르기에도 들어 있음 — 겹친 몫을 자동으로 빼지 않음" - "(파종 구간을 줄일지 설계자가 고름 · 2026-09-15 브레인 ⓑ)" + "녹화와 초류종자살포는 병행시공(임도기술교본 7장) — 떼 면적을 파종에서 빼지 않음." + " 실무도 같음(울진1공구 수량산출)" ) NOTE_STAKE = ( "평떼 5-22-3 [주] 「평떼(0.06인) 품에는 떼꽂이 포함」은 5-12 품에 걸린 것으로 읽음" @@ -97,6 +114,13 @@ def revegetation_from_designs( for key, attr in ((CUT, "cut_length_m"), (FILL, "fill_length_m")): points = [(s.chainage_m, float(getattr(s, attr))) for s in ordered] options[key] = round(_interval_area(points, start, end), 4) + # 사면 낙차(직고) × 거리 — 선떼 연장의 밑수(5-16-1 [주]② 「대상지 직고」 · 양쪽 사면 합). + for key, role in ((CUT_RISE, "cut"), (FILL_RISE, "fill")): + points = [ + (s.chainage_m, sum(abs(g.rise_m) for g in s.segments if g.role == role)) + for s in ordered + ] + options[key] = round(_interval_area(points, start, end), 4) notes.append( f"구간 {start:g}~{end:g}m 사면 면적 — 절토면 {options[CUT]:,.2f}㎡ · 성토면" f" {options[FILL]:,.2f}㎡ (B06 측점 사면길이 · 평균단면적법)" @@ -114,6 +138,8 @@ def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]: method = str(options.get("method") or "") if not method: return [], ["녹화 공법을 안 고름 — 공법을 고르면 섬"] + if method == "선떼붙이기": + return _sod_wall(options) if method not in AREA_METHODS: template = NOTE_LENGTH if method in LENGTH_METHODS else NOTE_LATER return [], [template.format(method=method), NOTE_FIGURES] @@ -137,6 +163,65 @@ def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]: return components, notes +def _sod_length(options: dict[str, Any]) -> float | None: + """선떼 연장(m) = Σ 사면 낙차 × 거리 ÷ 단 직고 — 면·단 직고가 없으면 None.""" + keys = RISES.get(str(options.get("face") or "")) + step = _num(options.get("step_height_m")) + if keys is None or step <= 0: + return None + return sum(_num(options.get(key)) for key in keys) / step + + +def _sod_wall(options: dict[str, Any]) -> tuple[list[Component], list[str]]: + """선떼붙이기 — 연장(m) · 떼(매) · 떼붙임(㎡).""" + if str(options.get("face") or "") not in RISES: + return [], [NOTE_FACE] + missing = [ + label + for key, label in ( + ("step_height_m", "단 직고"), + ("sod_grade", "급수"), + ("sod_size", "떼 크기"), + ) + if options.get(key) in (None, "", 0) + ] + grade, size = str(options.get("sod_grade") or ""), str(options.get("sod_size") or "") + if not missing and (size not in SOD_TABLE or not grade.rstrip("급").isdigit()): + missing = ["급수·떼 크기"] + if missing: + return [], [NOTE_SOD_MISSING.format(what="·".join(missing))] + length = _sod_length(options) or 0.0 + spec, sheet_area, per_m = SOD_TABLE[size] + sheets = per_m[int(grade.rstrip("급")) - 1] + notes = [NOTE_OVERLAP, NOTE_SOD, NOTE_SOD_TABLE_GAP, NOTE_SOD_HAUL, NOTE_FIGURES] + rows = [ + ( + "선떼붙이기", + "m", + length, + f"{options.get('face')} 사면 낙차 × 거리 ÷ 단 직고" + f" {_num(options.get('step_height_m')):g}", + "", + ), + ("떼", "매", length * sheets, f"연장 {length:,.2f} × {grade} {size} {sheets:g}매/m", spec), + ( + "떼붙임", + "㎡", + length * sheets * sheet_area, + f"떼 {length * sheets:,.1f}매 × 한 장 {sheet_area:g}㎡", + "", + ), + ] + components = [ + Component(item, unit, amount, destination, text, spec=item_spec) + for item, unit, amount, text, item_spec in rows + if (destination := routed(item, notes)) + ] + return components, notes + + def billing(options: dict[str, Any]) -> tuple[str, float]: - """내역 줄 — 녹화 면적 ㎡.""" + """내역 줄 — 녹화 면적 ㎡ · 선떼붙이기는 연장 m.""" + if str(options.get("method") or "") == "선떼붙이기": + return "m", _sod_length(options) or 0.0 return "㎡", _area(options) or 0.0 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 7a272c70..55e5d1a0 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 @@ -570,6 +570,29 @@ "why": "줄떼 5-23 — 떼(0.3×0.3 · 30㎝ 간격 3.33매/㎡)는 자재 축 · 붙임·떼꽂이 ㎡ 묶음(2026-09-15 브레인 녹화 ⓓ).", "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" }, + { + "type_id": "revegetation", + "when": {"method": "선떼붙이기"}, + "parts": [ + { + "code": "FP-05-16-01", + "name": "단끊기", + "unit": "m", + "from_components": ["선떼붙이기"], + "kind_suffix": "cutting_soil" + }, + { + "code": "FP-05-16-02", + "name": "선떼붙이기 떼붙임", + "unit": "㎡", + "from_components": ["떼붙임"], + "not_ready": true, + "why": "떼붙임 호표 없음 — 5-16-2 표가 「물량산출」이라 수량을 안 줌(수량은 섬 · 금액만 안 섬 · 2026-09-15 브레인 ⓓ)" + } + ], + "why": "선떼붙이기 5-16-2(10m당) = 단끊기 m 1(5-16-1 토질 갈래) + 떼붙임 ㎡ 「물량산출」 + 떼운반 — 떼 매수는 사방기술교본 표 2-2-6(급·떼 크기) · 떼는 자재 축.", + "needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김" + }, { "type_id": "revegetation", "when": {"method": "비탈덮기", "cover_kind": "거적"}, diff --git a/resources/tester/test_b05_not_in_table_marks.py b/resources/tester/test_b05_not_in_table_marks.py index a46bb910..358c0dd9 100644 --- a/resources/tester/test_b05_not_in_table_marks.py +++ b/resources/tester/test_b05_not_in_table_marks.py @@ -42,11 +42,6 @@ MARKED: dict[str, tuple[dict, list[str]]] = { "weep_hole_area_m2", ], ), - # 2026-09-15 녹화 ㎡ 넷 — 단 직고·선떼 급수는 길이형(선떼·조공) 산출식이 설 때 읽힘. - "revegetation": ( - {"method": "평떼", "face": "양쪽", "b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0}, - ["step_height_m", "sod_grade"], - ), "soil_guard": ( {"height_m": 1.0, "length_m": 10.0, "form": "떼"}, [ diff --git a/resources/tester/test_b08_revegetation_unit.py b/resources/tester/test_b08_revegetation_unit.py index 039dc7dd..06a00db7 100644 --- a/resources/tester/test_b08_revegetation_unit.py +++ b/resources/tester/test_b08_revegetation_unit.py @@ -48,7 +48,9 @@ def test_평떼_양쪽은_면적과_떼_11매() -> None: assert got[("평떼", "㎡")] == pytest.approx(100.0) assert got[("떼", "매")] == pytest.approx(1100.0) text = " ".join(row["notes"]) - assert "겹친 몫을 자동으로 빼지 않음" in text and row["notes"][0].startswith("⚠") + # 병행시공(임도기술교본 7장 · 울진1공구) — 흠이 아니라 정보(⚠ 안 붙임 · 안티그래비티 확인) + assert row["notes"][0].startswith("녹화와 초류종자살포는 병행시공(임도기술교본 7장)") + assert "떼 면적을 파종에서 빼지 않음" in text and "⚠" not in row["notes"][0] assert "5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈" in text parts = _parts(table) assert parts["FP-05-22-02"]["quantity"] == pytest.approx(100.0) @@ -80,10 +82,63 @@ def test_면을_안_고르면_사유() -> None: assert not row["components"] and any("절토면" in n and "성토면" in n for n in row["notes"]) -def test_길이형은_떼_매수표를_기다린다() -> None: - row = _row(method="선떼붙이기", face="양쪽")["structures"][0] +def test_조공은_아직_산출식이_없다() -> None: + row = _row(method="조공", face="양쪽")["structures"][0] assert not row["components"] - assert any("사방기술교본" in n for n in row["notes"]) + assert any("산출식 아직 없음" in n for n in row["notes"]) + + +SOD = { + "method": "선떼붙이기", + "face": "양쪽", + "step_height_m": 1.5, + "sod_grade": "5급", + "sod_size": "대형 40×40", + "cutting_soil": "보통토사", + # 구간 안 사면 낙차 × 거리(m·m) — 절토 30 · 성토 60 ⇒ 연장 = 90 ÷ 1.5 = 60m + "b06_cut_rise_area_m2": 30.0, + "b06_fill_rise_area_m2": 60.0, +} + + +def test_선떼는_낙차_나누기_단_직고로_연장_표_2_2_6_매수() -> None: + """품셈 5-16-1 [주]② 연장 = 대상지 직고 ÷ 단 직고 × 연장 · 표 2-2-6 표 값(식 아님).""" + table = _row(**SOD) + row = table["structures"][0] + got = {(c["name"], c["unit"]): c for c in row["components"]} + assert (row["billing_unit"], row["billing_quantity"]) == ("m", pytest.approx(60.0)) + assert got[("선떼붙이기", "m")]["amount"] == pytest.approx(60.0) + # 5급 대형 표 값 7.80매/m(단면식 3.0 × 2.5 = 7.50 과 어긋남 — 표가 이김) + assert got[("떼", "매")]["amount"] == pytest.approx(60.0 * 7.80) + assert got[("떼", "매")]["spec"] == "40×40" + assert got[("떼붙임", "㎡")]["amount"] == pytest.approx(60.0 * 7.80 * 0.16) + text = " ".join(row["notes"]) + assert "5급 대형 표 7.80 ↔ 단면식 7.50" in text and "소형 2급 표 27.0 ↔ 단면 4.0" in text + parts = _parts(table) + assert parts["FP-05-16-01#보통토사"]["quantity"] == pytest.approx(60.0) + # 떼붙임은 수량만 — 호표가 없어 막힘 + 사유(금액 안 섬) + assert parts["FP-05-16-02"]["quantity"] == pytest.approx(60.0 * 7.80 * 0.16) + assert parts["FP-05-16-02"]["not_ready"] + assert "떼붙임 호표 없음" in parts["FP-05-16-02"]["why"] + + +def test_선떼_소형_2급은_표_값_27() -> None: + table = _row(**{**SOD, "sod_grade": "2급", "sod_size": "소형 18×18"}) + got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]} + assert got[("떼", "매")] == pytest.approx(60.0 * 27.0) + assert got[("떼붙임", "㎡")] == pytest.approx(60.0 * 27.0 * 0.18 * 0.18) + + +@pytest.mark.parametrize("missing", ["step_height_m", "sod_grade", "sod_size"]) +def test_선떼_칸이_비면_사유(missing: str) -> None: + row = _row(**{k: v for k, v in SOD.items() if k != missing})["structures"][0] + assert not row["components"] and row["notes"] + + +def test_단끊기_토질을_안_고르면_그_조각만_막힘() -> None: + table = _row(**{k: v for k, v in SOD.items() if k != "cutting_soil"}) + part = _parts(table)["FP-05-16-01"] + assert part["not_ready"] and "토질" in part["why"] def test_구간_안_사면_면적은_B06_측점에서_보간() -> None: @@ -104,6 +159,22 @@ def test_구간_안_사면_면적은_B06_측점에서_보간() -> None: assert filled["options"]["b06_fill_area_m2"] == pytest.approx(22.5) +def test_구간_안_사면_낙차_면적도_B06_측점에서() -> None: + from B08_Quantity.B08_Quantity_Engine_SlopeLength import SlopeSegment, StationSlope + + def seg(role: str, rise: float) -> SlopeSegment: + return SlopeSegment("left", role, 0.0, 1.0, rise, 1.0, 1.0) + + slopes = [ + StationSlope(chainage_m=0.0, segments=(seg("cut", 2.0), seg("fill", -4.0))), + StationSlope(chainage_m=20.0, segments=(seg("cut", 4.0), seg("cut", 1.0))), + ] + structure = {"type_id": "revegetation", "start_m": 0.0, "end_m": 20.0, "options": {}} + options = revegetation_from_designs(structure, [], slopes)["options"] + assert options["b06_cut_rise_area_m2"] == pytest.approx((2.0 + 5.0) / 2 * 20) + assert options["b06_fill_rise_area_m2"] == pytest.approx((4.0 + 0.0) / 2 * 20) + + def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인다() -> None: options = {o.key: o for o in structure_type_map()["revegetation"].options} assert "초류종자 살포" not in options["method"].choices @@ -111,5 +182,12 @@ def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인 assert ( options["face"].choices == ["절토면", "성토면", "양쪽"] and options["face"].default is None ) - for key in ("step_height_m", "sod_grade"): - assert options[key].default is None and "아직 표에 안 쓰임" in options[key].not_in_table + for key in ("step_height_m", "sod_grade", "sod_size", "cutting_soil"): + assert options[key].default is None and not options[key].not_in_table + assert options["sod_size"].choices == ["대형 40×40", "소형 18×18"] + # 단끊기 토질은 품셈 5-16-1 표 갈래 이름 그대로(B06 지반 프리셋과 다른 축) + assert options["cutting_soil"].choices == [ + "보통토사", + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사", + ]