feat(master_data): 산림품셈 13장 로직 36줄 · 기계 경비 로직 둘(일반 · 굴착기+부착용 집게)

- 로직_산림품셈_13장: 13-2 ~ 13-16 돈 로직 35 · 작업량 로직 1 (13-4-1 그대로)
- 로직_건설품셈_공통8장: 8 기계 시간당 경비 · 8 굴착기+부착용 집게 시간당 경비(잡재료 16% · 집게 손료)
- 계수: 8-4-8 주⑤ 조합 잡재료율 · 13-13-2 비율
- 로직_산림품셈_9장: 인력 10% · 장비 90% 줄 비고 「해석: 전량 인력 품 × 비율」
- master_formula: 범위규칙 「이하 · 칸마다 위 끝만」 · 「마지막 칸 이하 포함」
- 시험 셋 더함(14 통과)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-19 16:32:17 +09:00
co-authored by Claude Opus 5
parent 2fe69782b6
commit 83b0083f4c
7 changed files with 1901 additions and 12 deletions
@@ -251,11 +251,18 @@ def _match(table: dict, row: dict, cond: str, want) -> bool:
have = row[cond]
if table.get("조건", {}).get(cond) == "범위":
low, high = have
rule = table.get("범위규칙", "초과·이하").split(" · ")[0]
rule, *more = table.get("범위규칙", "초과·이하").split(" · ")
if rule == "초과·이하":
return (low is None or want > low) and (high is None or want <= high)
if rule == "이상·미만":
return (low is None or want >= low) and (high is None or want < high)
# 「마지막 칸(80~100)은 이하 포함」 — 가장 윗줄만 위 끝 포함
top = max(r[cond][1] for r in table[""] if r.get(cond) and r[cond][1] is not None)
closed = any("이하 포함" in x for x in more) and high == top
return (low is None or want >= low) and (
high is None or want < high or (closed and want == high)
)
if rule == "이하": # 칸마다 위 끝만 — 맞는 줄 가운데 위 끝이 가장 작은 줄(find)
return (low is None or want > low) and (high is None or want <= high)
raise FormulaError(f"모르는 범위규칙 「{table.get('범위규칙')}")
return have == want
@@ -265,6 +272,11 @@ def find(table: dict, conds: dict) -> dict:
if cond not in table.get("조건", {}):
raise FormulaError(f"{table['열쇠']}」 에 없는 조건 「{cond}")
hits = [row for row in table[""] if all(_match(table, row, c, v) for c, v in conds.items())]
if len(hits) > 1 and table.get("범위규칙", "").split(" · ")[0] == "이하":
for c in conds:
ups = [r[c][1] for r in hits if table["조건"].get(c) == "범위" and r.get(c)]
if ups and None not in ups:
hits = [r for r in hits if not r.get(c) or r[c][1] == min(ups)]
if len(hits) != 1:
raise FormulaError(f"{table['열쇠']}{conds} 맞는 줄 {len(hits)}")
return hits[0]
@@ -22,6 +22,18 @@
"① 전용덤프트럭(18톤이상)과 불도저(19톤이상)의 경우는 보정하지 않는다. 단, 타이어 불도저, 습지 불도저는 보정한다.",
"② 전석섞인 토사는 전석(0.5㎥이상)의 혼입율이 30%이상 말한다."
]
},
{
"열쇠": "8-4-8 주⑤ 조합 잡재료율",
"이름": "불도저·굴착기에 리퍼·브레이커·부착용 집게 조합 시 잡재료 비율",
"기준": "",
"출처": "건설품셈 공통 8-4-8 주⑤",
"조건": { "구분": "고르기" },
"값칸": { "적용": "%" },
"줄": [{ "구분": "불도저 및 굴착기 잡재료 비율", "적용": 16 }],
"주": [
"⑤ 불도저 및 굴착기에 리퍼, 브레이커, 부착용집게를 조합하여 사용할 때는 불도저 및 굴착기의 잡재료 비율을 16%로 계상하고, 리퍼, 브레이커, 부착용 집게의 손료 및 치즐 소모율을 추가하는 것이다."
]
}
]
}
@@ -363,6 +363,20 @@
{ "구분": "공구손료 및 경장비(봉합기) 기계경비(인력품의)", "적용": 4, "단위": "%" }
],
"주": []
},
{
"열쇠": "13-13-2 비율",
"이름": "토양개량제 부설 인력·장비 비율",
"기준": "",
"출처": "산림품셈 13-13-2",
"조건": { "구분": "고르기" },
"값칸": { "적용": "단위 칸 따름", "단위": "글" },
"줄": [
{ "구분": "인력", "적용": 10, "단위": "%" },
{ "구분": "장비", "적용": 90, "단위": "%" },
{ "구분": "굴착기 버킷", "적용": 0.2, "단위": "㎥" }
],
"주": ["표 머리 「인력(10%)」 · 「장비(90%)」 · 「굴착기(0.2㎥)」"]
}
]
}
@@ -61,6 +61,160 @@
{ "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }
],
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음"
},
{
"열쇠": "8 기계 시간당 경비",
"이름": "기계 시간당 기계경비(손료 보정 없는 기종)",
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
"소유": "공용",
"입력": [
{ "이름": "기계", "고르기": ["1730-0015", "7204-0055"] },
{
"이름": "지역",
"고르기": [
"전국평균",
"전국",
"서울",
"경기",
"강원",
"충북",
"충남",
"전북",
"경북",
"경남",
"부산",
"제주",
"대구",
"인천",
"대전",
"울산",
"세종",
"전남광주"
],
"출처": "건설품셈 공통 8-1-7 5"
}
],
"중간": [
{
"이름": "손료",
"식": "버림(기계:건설품셈:{기계}.가격 * 1000 * ((기계:건설품셈:{기계}.상각비계수 + 기계:건설품셈:{기계}.정비비계수) + 기계:건설품셈:{기계}.관리비계수) / 10000000, 0)",
"출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4"
},
{ "이름": "연료", "식": "기계:건설품셈:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
{ "이름": "직종", "식": "기계:건설품셈:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" }
],
"호표": [
{
"종류": "재료",
"요소": "재료:오피넷유가:{연료}:{지역}",
"이름": "주연료",
"단위": "",
"수량": "기계:건설품셈:{기계}.주연료",
"비목": "재료비",
"출처": "건설품셈 공통 8-4-1"
},
{
"종류": "인력",
"요소": "인력:건설노임:{직종}",
"이름": "조종원",
"단위": "인",
"수량": "기계:건설품셈:{기계}.조종원 / 8",
"비목": "노무비",
"출처": "건설품셈 공통 8-1-3 6",
"비고": "환산 근거 미확인 — 우선 하루 ÷ 8"
}
],
"덧줄": [
{ "이름": "잡재료비", "식": "재료비 * 기계:건설품셈:{기계}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②" },
{ "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" }
],
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음",
"비고": "고르기는 쓰는 기계만 — 주연료·조종원이 있는 기계는 더 넣어 씀"
},
{
"열쇠": "8 굴착기+부착용 집게 시간당 경비",
"이름": "굴착기(무한궤도)+부착용 집게 시간당 기계경비",
"결과단위": "원/시간",
"출처": "건설품셈 공통 8-1-5 · 8-1-6 · 8-1-7 · 8-4-8 주⑤",
"소유": "공용",
"입력": [
{ "이름": "기계", "고르기": ["0201-0060"] },
{ "이름": "집게", "고르기": ["7206-0070"] },
{
"이름": "지역",
"고르기": [
"전국평균",
"전국",
"서울",
"경기",
"강원",
"충북",
"충남",
"전북",
"경북",
"경남",
"부산",
"제주",
"대구",
"인천",
"대전",
"울산",
"세종",
"전남광주"
],
"출처": "건설품셈 공통 8-1-7 5"
},
{ "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" }
],
"중간": [
{
"이름": "가산비율",
"식": "만약(보정작업 == '해당 없음', 0, 찾기(계수:건설품셈:8-1-7 손료보정, 기종='굴착기(무한궤도) 및 로더(무한궤도)', 작업=보정작업).가산비율)",
"출처": "건설품셈 공통 8-1-7 1"
},
{
"이름": "손료",
"식": "버림(기계:건설품셈:{기계}.가격 * 1000 * ((기계:건설품셈:{기계}.상각비계수 + 기계:건설품셈:{기계}.정비비계수) * (1 + 가산비율 / 100) + 기계:건설품셈:{기계}.관리비계수) / 10000000, 0)",
"출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4 · 8-1-7 1"
},
{
"이름": "집게손료",
"식": "버림(기계:건설품셈:{집게}.가격 * 1000 * ((기계:건설품셈:{집게}.상각비계수 + 기계:건설품셈:{집게}.정비비계수) + 기계:건설품셈:{집게}.관리비계수) / 10000000, 0)",
"출처": "건설품셈 공통 8-1-5 12 · 8-4-8 주⑤"
},
{ "이름": "조합잡재료율", "식": "찾기(계수:건설품셈:8-4-8 주⑤ 조합 잡재료율, 구분='불도저 및 굴착기 잡재료 비율').적용", "출처": "건설품셈 공통 8-4-8 주⑤" },
{ "이름": "연료", "식": "기계:건설품셈:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰" },
{ "이름": "직종", "식": "기계:건설품셈:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5" }
],
"호표": [
{
"종류": "재료",
"요소": "재료:오피넷유가:{연료}:{지역}",
"이름": "주연료",
"단위": "",
"수량": "기계:건설품셈:{기계}.주연료",
"비목": "재료비",
"출처": "건설품셈 공통 8-4-1"
},
{
"종류": "인력",
"요소": "인력:건설노임:{직종}",
"이름": "조종원",
"단위": "인",
"수량": "기계:건설품셈:{기계}.조종원 / 8",
"비목": "노무비",
"출처": "건설품셈 공통 8-1-3 6",
"비고": "환산 근거 미확인 — 우선 하루 ÷ 8"
}
],
"덧줄": [
{ "이름": "잡재료비", "식": "재료비 * 조합잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4-8 주⑤" },
{ "이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2" },
{ "이름": "집게손료", "식": "집게손료", "비목": "경비", "출처": "건설품셈 공통 8-4-8 주⑤" }
],
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음",
"비고": "집게는 굴착기 부수물(8-3-1 (0250) 주 · 규격은 해당 굴착기 규격) — 0.6∼0.8 집게를 0.6㎥ 굴착기에 붙임"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -440,7 +440,8 @@
}
],
"덧줄": [],
"끝수": null
"끝수": null,
"비고": "해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-1 구조물터파기 육상토사(01m) 작업량",
@@ -515,7 +516,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-2 구조물터파기 육상토사(12m) 작업량",
@@ -590,7 +591,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "장비 「육상토사(01m)와 동일」 — 9-13-1 표 · 비율도 9-13-1 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "장비 「육상토사(01m)와 동일」 — 9-13-1 표 · 비율도 9-13-1 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-3 구조물터파기 육상토사(23m) 작업량",
@@ -665,7 +666,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "장비 「육상토사(01m)와 동일」 — 9-13-1 표 · 비율도 9-13-1 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "장비 「육상토사(01m)와 동일」 — 9-13-1 표 · 비율도 9-13-1 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-4 구조물터파기 용수토사(01m) 작업량",
@@ -740,7 +741,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-5 구조물터파기 용수토사(12m) 작업량",
@@ -815,7 +816,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "장비 「용수토사(01m)와 동일」 — 9-13-4 표 · 비율도 9-13-4 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "장비 「용수토사(01m)와 동일」 — 9-13-4 표 · 비율도 9-13-4 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-13-6 구조물터파기 용수토사(23m) 작업량",
@@ -890,7 +891,7 @@
],
"덧줄": [],
"끝수": null,
"비고": "장비 「용수토사(01m)와 동일」 — 9-13-4 표 · 비율도 9-13-4 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단)"
"비고": "장비 「용수토사(01m)와 동일」 — 9-13-4 표 · 비율도 9-13-4 · 9-13 머리 ※ 관보호공·관부설 보통인력 30% 가산 미반영(적용 여부 설계자 판단) · 해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-14-1 되메우기 작업량",
@@ -964,7 +965,8 @@
}
],
"덧줄": [],
"끝수": null
"끝수": null,
"비고": "해석: 전량 인력 품 × 비율"
},
{
"열쇠": "9-14-2 다짐 작업량",
+34 -2
View File
@@ -97,6 +97,35 @@ def test_분수_글자_값(whole):
assert mf.run(whole, "산림품셈:9-4-2 암절취 집토 작업량", {})["결과"] == Decimal("17.325")
def test_범위규칙_이하_위끝만(whole):
"""13-4-2 뒷길이 「35cm 이하 · 55cm 이하」 — 36 은 55 칸."""
region = {"지역": "전국평균", "보정작업": "해당 없음"}
qty = [
mf.run(whole, "산림품셈:13-4-2 메쌓기(장비)", {"뒷길이": x, **region})[""][0]["수량"]
for x in (35, 36, 75)
]
assert qty == [Decimal("0.1"), Decimal("0.09"), Decimal("0.08")]
def test_범위규칙_마지막칸_이하_포함(whole):
"""13-6-1 직경 「80㎝이상∼100㎝이하」 — 100 은 맞고 101 은 막힘."""
given = {"직경": 100, "지역": "전국평균", "보정작업": "해당 없음"}
assert mf.run(whole, "산림품셈:13-6-1 큰돌 메쌓기", given)[""][0]["수량"] == Decimal("0.68")
with pytest.raises(mf.FormulaError):
mf.run(whole, "산림품셈:13-6-1 큰돌 메쌓기", {**given, "직경": 101})
def test_굴착기_부착용_집게(whole):
"""잡재료 16%(8-4-8 주⑤) · 집게 손료 7,783천원×7435×10⁻⁷ 버림."""
got = mf.run(
whole,
"건설품셈:8 굴착기+부착용 집게 시간당 경비",
{"기계": "0201-0060", "집게": "7206-0070", "지역": "전국평균", "보정작업": "해당 없음"},
)
assert got["중간"]["집게손료"] == 5786
assert cents(got[""]) == "86467.61"
def test_범위_입력은_막음(whole):
with pytest.raises(mf.FormulaError):
mf.run(
@@ -113,9 +142,12 @@ def test_견본_파일_검사_통과(whole):
# 9장 계수 표는 같은 절의 소요량 표와 함께 대조 — 소요량 파일 자체의 알림은 그 창 몫
with_09 = files | cm.load(["소요량_산림품셈_09장.json"])
# 절 통째를 출처로 둔 다른 창 표(9-13 · 9-20)의 결손도 그 창 몫
others = ("9-13 관보호공 인력 가산", "9-20 대상 근주")
# 8-4-8 주⑤ 표는 절의 운전경비 수가 기계 요소(표 그룹 밖)에 있어 결손으로 뜸
others = ("9-13 관보호공 인력 가산", "9-20 대상 근주", "8-4-8 주⑤ 조합 잡재료율")
body = cm.check_body(with_09)
assert [x for x in body if not x.startswith("소요량_산림품셈_09장") and not x.endswith(others)] == []
assert [
x for x in body if not x.startswith("소요량_산림품셈_09장") and not x.endswith(others)
] == []
assert cm.check_logics(files, whole) == []