feat(b05·b08): 선떼붙이기 — 연장 = 사면 낙차 × 거리 ÷ 단 직고(품셈 5-16-1 [주]②) · 떼 매수 = 사방기술교본 표 2-2-6 표 값(어긋남 둘 사유) · 떼 크기·단끊기 토질 칸 기본 없음 · 떼붙임 ㎡ 는 호표 없어 막힘 + 사유(브레인 ⓐ~ⓔ) · 녹화 병행시공 사유를 정보로

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-15 06:49:19 +09:00
co-authored by Claude Opus 5
parent 4d477c159f
commit 1395ae6d34
7 changed files with 237 additions and 24 deletions
@@ -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": "거적"},
@@ -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": "떼"},
[
+84 -6
View File
@@ -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 == [
"보통토사",
"경질ㆍ고사점토 및 자갈섞인 점토",
"호박돌 섞인 토사",
]