Files
Aislo/resources/tester/test_masterdata_check.py
T
eomsangdonandClaude Opus 5 a8a8e49b52 feat(master_data): 노임 준용 칸 · ESTX 다른 조사 다섯 갈래 노임 파일 · 인력_자체 → 새 노임 열쇠와 인력_미확보
- 역산: ESTX 미공표 추정 = 마지막 공표값 × 전체직종 평균 증가율 공표일마다 이어 곱·원 미만 버림 (5 직종 원 단위 일치 · 9 직종 앞 값 없음)
- 건설노임 미공표 14 줄에 준용 칸(null) · 엔진은 값이 비면 준용 직종 값 · 호표 출처 「준용: <직종>」 · M01 계산표에 표시
- 새 파일: 엔지니어링노임 59 · 측량노임 20 · 건설사업관리노임 7 · SW노임 17 · 산림노임 2 (줄 수·값 합 ESTX 와 같음)
- 인력_자체 49 → 새 노임 열쇠 18 · 인력_미확보 31 · 로직 참조 526 곳
- 로직 일괄 시험 계산: 통과 1011 → 1088 · 인력 값 없음 멈춤 176 → 84

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
2026-09-19 23:31:02 +09:00

627 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""마스터 데이터 검사 도구 — 견본 셋 손 계산 · 견본 파일 검사 통과 (`resources/master_data/scripts/check_master.py`)."""
import sys
from decimal import ROUND_HALF_UP, Decimal
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts"))
import check_master as cm # noqa: E402
import master_formula as mf # noqa: E402
SAMPLES = [
"계수_건설품셈_공통8장.json",
"계수_산림품셈_9장.json",
"기계_건설품셈.json",
"로직_건설품셈_공통3장.json",
"로직_건설품셈_공통4장.json",
"로직_건설품셈_공통7장.json",
"로직_건설품셈_공통8장.json",
"로직_건설품셈_토목1장.json",
"로직_건설품셈_토목2장.json",
"로직_건설품셈_토목3장.json",
"로직_건설품셈_토목4장.json",
"로직_건설품셈_토목5장.json",
"로직_건설품셈_토목6장.json",
"로직_건설품셈_토목7장.json",
"로직_건설품셈_토목8장.json",
"로직_건설품셈_토목9장.json",
"로직_산림품셈_03장.json",
"로직_산림품셈_04장.json",
"로직_산림품셈_05장.json",
"로직_산림품셈_06장.json",
"로직_산림품셈_07장.json",
"로직_산림품셈_08장.json",
"로직_산림품셈_10장.json",
"로직_산림품셈_11장.json",
"로직_산림품셈_13장.json",
"로직_산림품셈_14장.json",
"로직_산림품셈_9장.json",
"소요량_산림품셈_13장.json",
]
EXCAVATOR = "건설품셈:공통 8 굴착기(무한궤도) 시간당 경비"
@pytest.fixture(scope="module")
def whole():
return cm.master()
@pytest.fixture(scope="module")
def faked():
"""미확보 인력 값이 빈 칸 — 시험용 가짜 값(파일엔 안 씀)."""
files = cm.load()
for row in files["인력_미확보.json"]["줄"]:
if row["값"] is None:
row["값"] = Decimal(1000)
return mf.Master(files)
def cents(x) -> str:
return str(Decimal(x).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
def test_메쌓기(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 2, "초과증가율": 80},
)
assert cents(got["계"]) == "203281.20"
def test_메쌓기_높이_증가율(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 5, "초과증가율": 80},
)
assert [line["수량"] for line in got["줄"]] == [Decimal("0.70"), Decimal("0.56")]
def test_굴착기(whole):
got = mf.run(
whole, EXCAVATOR, {"기계": "0201-0070", "지역": "전국평균", "보정작업": "해당 없음"}
)
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
"35412.13",
"26130.11",
"24554.00",
"86096.24",
]
def test_굴착기_암석_보정(whole):
got = mf.run(
whole,
EXCAVATOR,
{"기계": "0201-0070", "지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"},
)
assert got["중간"]["손료"] == 28322
assert cents(got["계"]) == "89864.24"
def test_불도저_덤프_로더_손료_보정(whole):
# 손료 = 가격 × 1000 × ((상각 + 정비) × (1 + 가산) + 관리) ÷ 10^7 · 원 미만 버림(8-1-7 1)
ref = "건설품셈:공통 8 기계 시간당 경비(손료 보정)"
rock, boulder = "암석작업(연암・보통암・경암)", "전석섞인토사"
for machine, work, cost in (
("0602-0150", rock, 24285), # 덤프트럭 25%
("0602-0150", "해당 없음", 20659),
("0101-0010", boulder, 32794), # 불도저 10%
("0301-0095", rock, 20985), # 로더(무한궤도) 20%
):
given = {"기계": machine, "지역": "전국평균", "보정작업": work}
assert mf.run(whole, ref, given)["중간"]["손료"] == cost
plain = mf.run(
whole, "건설품셈:공통 8 기계 시간당 경비", {"기계": "0602-0150", "지역": "전국평균"}
)
assert plain["중간"]["손료"] == 20659
# 주① 불도저 19톤 이상은 가산 0
big = {"기계": "0101-0019", "지역": "전국평균"}
assert (
mf.run(whole, ref, {**big, "보정작업": rock})["중간"]["손료"]
== mf.run(whole, "건설품셈:공통 8 기계 시간당 경비", big)["중간"]["손료"]
)
def test_손료만_로직_보정과_불도저_운반(whole):
rock = "암석작업(연암・보통암・경암)"
ref = "건설품셈:공통 8 기계 시간당 손료(손료 보정)"
assert mf.run(whole, ref, {"기계": "0201-0070", "보정작업": rock})["중간"]["손료"] == 28322
assert mf.run(whole, ref, {"기계": "7620-0003", "보정작업": rock})["중간"]["손료"] == 1232
# 산림 10-11 발파암 = 암석작업 보정 경비 × 1/작업량
given = {"재료": "발파암", "기계": "0101-0010", "지역": "전국평균"}
got = mf.run(whole, "산림품셈:10-11 불도저 운반", given)
hour = mf.run(
whole,
"건설품셈:공통 8 기계 시간당 경비(손료 보정)",
{"기계": "0101-0010", "지역": "전국평균", "보정작업": rock},
)
assert got["줄"][0]["단가"] == hour["계"]
def test_노체포설(whole):
assert mf.run(whole, "산림품셈:9-16-1 노체포설 작업량", {})["결과"] == Decimal("24.255")
got = mf.run(whole, "산림품셈:9-16-1 노체포설", {"지역": "전국평균", "보정작업": "해당 없음"})
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "계")] == [
"1459.99",
"1077.31",
"1012.33",
"3549.63",
]
def test_범위규칙_이상_미만(whole):
"""9-2 횡단경사 「60% 미만 · 60% 이상」 — 60 은 윗줄."""
qty = [
mf.run(whole, "산림품셈:9-2 노선 굴진 보조원", {"횡단경사": x})["줄"][0]["수량"]
for x in (59.9, 60)
]
assert qty == [5, 10]
def test_분수_글자_값(whole):
"""9-4-2 f 원문 「1/1.40」 — 3600×0.7×0.55×(1/1.40)×0.35/20."""
assert mf.run(whole, "산림품셈:9-4-2 암절취 집토 작업량", {})["결과"] == Decimal("17.325")
def test_범위규칙_이하_위끝만(whole):
"""13-4-2 뒷길이 「35cm 이하 · 55cm 이하」 — 36 은 55 칸."""
region = {"지역": "전국평균", "보정작업": "해당 없음", "잡석량": 0}
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):
"""10-11 — q=3.2×0.96 · ㎝=20/55+20/70+0.25 · Q=60×q×(1/1.30)×0.55/㎝."""
got = mf.run(whole, "산림품셈:10-11 불도저 운반 작업량", {"재료": "토사"})
assert got["결과"] == Decimal("86.709")
def test_중기운반_트레일러_횟수(whole):
"""10-4 — ㎝=20+30+20+0.42 · N=60×0.9/㎝ · 1회 = 1/N 시간."""
got = mf.run(whole, "산림품셈:10-4 중기운반 트레일러", {"t2": 30, "지역": "전국평균"})
assert got["줄"][0]["수량"].quantize(Decimal("0.0001")) == Decimal("1.3041")
def test_예정지정리_할증_합산(whole):
"""3-5 — 바이오매스 10 + 침엽수림 10 + 개벌지 10 + 산불 5×2/10 = 31% · 2.4×50%×1.31."""
given = {
"작업내용": "보완사업지 정리",
"정리량": 120,
"임상": "침엽수림",
"개벌지": "700㎡ 이하(또는 벌채폭 30m)",
"산불피해면적": 2,
"조림면적": 10,
}
got = mf.run(whole, "산림품셈:3-5 예정지정리", given)
assert [line["수량"] for line in got["줄"]] == [Decimal("1.572"), Decimal("1.572")]
def test_작업안전_보조_올림(whole):
"""4-6 — 벌목부 4인당 보통인부 1인 · 5인이면 2인."""
qty = [
mf.run(whole, "산림품셈:4-6 벌목부 작업안전 보조", {"벌목부": n})["줄"][0]["수량"]
for n in (3, 4, 5, 8)
]
assert qty == [1, 1, 2, 2]
def test_동력상하차기_잔존목_할증(whole):
"""7-11 — 40m · 4160㎥ · 1.2m = 22.03㎥ · 잔존목 10% · 소모성경비 0.02105×380만/50."""
given = {
"최대집재거리": 40,
"집재재적": 50,
"원목길이": 1.2,
"잔존목": "잔존목 100본 이상 130본 이하",
"지역": "전국평균",
}
got = mf.run(whole, "산림품셈:7-11 동력상하차기 집재", given)
assert got["줄"][0]["수량"] == Decimal("1.1") / Decimal("22.03")
assert got["중간"]["소모성경비"] == Decimal("1599.8")
def test_방제_벌목조재_감_할증(whole):
"""8-3 — 경급 6 = 1.13㎥/인 · 소군락 −30% · 잣나무 +20% · 벌목부 50%."""
given = {
"경급": 6,
"소군락": "소군락 모두베기",
"방제수종": "잣나무",
"체인톱가격": 1200000,
"체인오일단가": 5000,
"지역": "전국평균",
}
got = mf.run(whole, "산림품셈:8-3 벌목조재", given)
want = Decimal("0.7") * Decimal("1.2") * Decimal("0.5") / Decimal("1.13")
assert got["줄"][0]["수량"].quantize(Decimal("0.000001")) == want.quantize(Decimal("0.000001"))
def test_건설_석재판_습식_공구손료(whole):
"""공통 7-4-1 — 바닥부 테라조판 석공 3 · 줄눈공 1 · 보통인부 2 ÷ 15㎡ · 공구손료 인력품 2%."""
got = mf.run(whole, "건설품셈:공통 7-4-1 습식공법", {"시공부위": "바닥부", "자재": "테라조판"})
assert [line["수량"] for line in got["줄"][:3]] == [
Decimal(3) / 15,
Decimal(1) / 15,
Decimal(2) / 15,
]
assert got["경비"] == got["노무비"] * Decimal("0.02")
def test_건설_정원석_난이도_가산(whole):
"""공통 4-4-1 — 쌓기 20ton 미만 조경공 1.212 × 1.1 · 굴착기 0.7㎥ 0.657hr 그대로."""
given = {
"작업": "쌓기",
"중량": "20ton 미만",
"난이도가산": 10,
"지역": "전국평균",
"보정작업": "해당 없음",
}
got = mf.run(whole, "건설품셈:공통 4-4-1 정원석 쌓기 및 놓기", given)
assert [line["수량"] for line in got["줄"][:2]] == [Decimal("1.3332"), Decimal("0.657")]
def test_건설_벌목_집재거리_가산(whole):
"""공통 3-9-1 — 나무높이 6m 벌목부 2.8 · 집재 250m = 100m 초과 두 번 × 30%."""
given = {"나무높이": 6, "집재거리": 250, "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:공통 3-9-1 벌목", given)
assert got["줄"][0]["수량"] == Decimal("4.48")
def test_건설_흙깎기_암_치즐(whole):
"""공통 3-2-3 — 풍화암 65㎥/일 · 굴착기+대형브레이커 8/65 시간 · 치즐 1%."""
given = {"암분류": "풍화암", "집토": "안 함", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:공통 3-2-3 흙깎기(기계) 암", given)
brk = got["줄"][1]
assert brk["수량"] == Decimal(8) / 65
assert got["줄"][-1]["금액"] == brk["금액"] / 100
def test_토목_동상방지층_인력_살수(whole):
"""토목 1-2-1 — 165㎥/일 · 인력 살수면 살수차 0 · 보통인부 100㎡당 1인 = 두께 20㎝ 에서 1/20 인/㎥."""
given = {"살수": "인력 살수", "두께": 20, "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:토목 1-2-1 인력식 소규모장비 포설", given)
qty = {line["이름"]: line["수량"] for line in got["줄"]}
assert qty["굴착기 0.6㎥"] == Decimal(8) / 165
assert qty["살수차 5,500"] == 0
assert got["줄"][-1]["수량"] == Decimal(1) / 20
def test_토목_보도블록_감(whole):
"""토목 1-7-1 — B-Type 190㎡/일 × 0.9(유도블록) · 정밀절단 아니면 특별인부 2 - 1."""
given = {
"형식": "B-Type",
"유도블록": "유도・점자블록",
"정밀절단": "아님",
"지역": "전국평균",
"보정작업": "해당 없음",
}
got = mf.run(whole, "건설품셈:토목 1-7-1 보도용 블록 설치(소형)", given)
assert got["줄"][1]["수량"] == Decimal(1) / Decimal(171)
def test_토목_블록붙이기_굴착기_대체(whole):
"""토목 2-3-3 주④ — 크레인 대신 굴착기 0.2㎥ 0.063hr · 크레인 0."""
given = {"장비": "굴착기", "크레인": "2104-0010", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:토목 2-3-3 블록 붙이기(기계)", given)
assert [line["수량"] for line in got["줄"][2:]] == [0, Decimal("0.063")]
def test_칸_이름_정리_뒤_기계_줄(whole):
"""공통 3-4-8 — 괄호·빈칸 칸 이름을 고친 뒤 모래지정은 플레이트 콤팩터만(8/110)."""
given = {"지정": "모래지정", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(whole, "건설품셈:공통 3-4-8 기초지정", given)
qty = {line["이름"]: line["수량"] for line in got["줄"]}
assert round(qty["플레이트 콤팩터 1.5ton"], 4) == Decimal("0.0727")
assert qty["진동롤러(핸드가이드식) 0.7ton"] == 0
def test_토목_관부설_시공조건_요율(whole):
"""토목 6-2-2 — 열쇠 「K.P」 점을 식이 읽음 · 시공조건 B 는 시공량 133%."""
given = {"관경": 80, "시공조건": "시공조건 B", "압륜가산": 0, "요율구간": "300mm이하"}
got = mf.run(whole, "건설품셈:토목 6-2-2 K.P 메커니컬 접합 및 부설 인력", given)
assert round(got["줄"][0]["수량"], 4) == round(Decimal(3) / (Decimal(13) * Decimal("1.33")), 4)
def test_토목_터널굴착_장대할증(whole):
"""토목 3-2-5 — A군 싸이클 480분 · 갱구 1,200m(α1 20%) = 1인 × 480/60/8 × 1.2."""
given = {
"공법군": "발파굴착A군",
"싸이클": 480,
"착암공인원": 3,
"보통인부인원": 3,
"갱구거리": 1200,
"병렬터널": "아님",
}
got = mf.run(whole, "건설품셈:토목 3-2-5 작업인원", given)
assert got["줄"][0]["수량"] == Decimal("1.2")
def test_토목_궤광조립_직결형_단선감(faked):
"""토목 4-3-1 — 직결형 시공량은 분리형 250m(병합 칸) · 단선 5% 감 = 궤도공 16 ÷ 237.5."""
given = {"단선감": 5, "구분": "침목매립식(직결형)", "지역": "전국평균", "보정작업": "해당 없음"}
got = mf.run(faked, "건설품셈:토목 4-3-1 궤광조립(콘크리트)", given)
assert got["줄"][0]["수량"] == Decimal(16) / Decimal("237.5")
def test_토목_강교_제작공수_보정(whole):
"""토목 5-1-2 — 연수 2 는 「이상·이하」 [2,2] · 총중량 표 범위규칙 뒤 괄호 설명은 뺌."""
given = {
"형식": "박스거더",
"고강도강비율": Decimal("0.5"),
"대형단품평균중량": 12,
"부품평균중량": 120,
"연수": 2,
"총중량": 600,
"사각": 60,
"곡률반경": 200,
"높이곡선변화": "있음",
"제작도": "별도 계상",
}
got = mf.run(whole, "건설품셈:토목 5-1-1 기본제작공수", given)
# 0.95 × (1 + 0.25×0.5) × 0.97 × 0.95 × (1 0.03 0.02 + 0.25 + 0.11)
want = Decimal("0.95") * Decimal("1.125") * Decimal("0.97") * Decimal("0.95") * Decimal("1.31")
assert got["줄"][0]["수량"] == want
def test_자체_직종으로_세운_절(faked):
"""건설노임에 없는 직종(고급·중급기술자 · 중급숙련기술자)을 엔지니어링노임으로 세운 줄."""
def qty(ref, given):
return {x["이름"]: x["수량"] for x in mf.run(faked, "건설품셈:" + ref, given)["줄"]}
# 3-11-1 조립 1일 + 해체 1일 — 인 × 2
got = qty("공통 3-11-1 머신 가이던스(MG) 굴착기 장비조립・해체", {})
assert [got[w] for w in ("고급기술자", "중급기술자", "용접공")] == [2, 2, 2]
# 5-1-3 주① 제작도 — 박스거더 0.4인/톤 × 본체 보정계수 1.31
given = {
"형식": "박스거더",
"고강도강비율": 0,
"대형단품평균중량": 12,
"부품평균중량": 120,
"연수": 2,
"총중량": 600,
"사각": 60,
"곡률반경": 200,
"높이곡선변화": "있음",
"제작도": "이 품에 포함",
}
assert qty("토목 5-1-1 기본제작공수", given)["중급숙련기술자"] == Decimal("0.4") * Decimal(
"1.31"
)
def test_토목_배송관_철거_크레인_규격(whole):
"""토목 7-4-1 — 관경 760 이상은 15톤 크레인 · 철거 감률은 인력·장비 모두."""
given = {
"관경": "840",
"접합방식": "플랜지접합",
"작업": "철거",
"철거감률": 30,
"지역": "전국평균",
}
got = {
x["이름"]: x["수량"] for x in mf.run(whole, "건설품셈:토목 7-4-1 배송관 접합", given)["줄"]
}
assert got["배관공(수도)"] == Decimal("0.12") * Decimal("0.7")
assert got["트럭탑재형 크레인 10ton"] == 0
assert got["트럭탑재형 크레인 15ton"] == Decimal("0.47") * Decimal("0.7")
def test_토목_보링_심도_가산():
"""토목 8-1-2 — 깊이 35m = 20m 넘는 15m 를 10m 단위 올림 2번 × 5% · 인력만 · 쵸핑비트는 호박돌층만."""
files = cm.load()
for row in files["인력_미확보.json"]["줄"] + files["재료_자체.json"]["줄"]:
if row["값"] is None: # 관리자가 채울 값 — 시험용 가짜 값(파일엔 안 씀)
row["값"] = Decimal(1000)
given = {"층": "점토층", "규격": "BX", "깊이": 35, "심도가산율": 5}
got = mf.run(mf.Master(files), "건설품셈:토목 8-1-2 천공(토사·자갈·호박돌층)", given)
qty = {x["이름"]: x["수량"] for x in got["줄"]}
assert qty["중급기술자"] == Decimal("0.16") * Decimal("1.1")
assert qty["싱글코아바렐"] == Decimal("0.01")
assert qty["쵸핑비트"] == 0
def test_토목_측량_계산예():
"""토목 9-1-2 원문 계산예 — 구하는점 6·주어진점 4·산지 = 인원 × 10/16 × 1.2 × 1.12 · 9-4-3-1 설계예 1km 당 × 연장."""
files = cm.load()
for row in files["인력_미확보.json"]["줄"]:
if row["값"] is None: # 관리자가 채울 값 — 시험용 가짜 값(파일엔 안 씀)
row["값"] = Decimal(1000)
master = mf.Master(files)
given = {"구하는점": 6, "주어진점": 4, "지형": "산지"}
got = {
x["이름"]: x["수량"]
for x in mf.run(master, "건설품셈:토목 9-1-2 1급 기준점 측량", given)["줄"]
}
assert [round(got[w], 2) for w in ("특급기술자", "고급기술자", "중급기술자", "인부")] == [
Decimal("2.52"),
Decimal("20.58"),
Decimal("33.60"),
Decimal("8.40"),
]
given = {"도로연장": 10, "지형": "그 외 지역"}
got = {
x["이름"]: x["수량"]
for x in mf.run(master, "건설품셈:토목 9-4-3-1 MMS측량 자료 이용", given)["줄"]
}
assert got["특급기술자"] == 17 and got["중급기술자"] == 198
def test_범위_입력은_막음(whole):
with pytest.raises(mf.FormulaError):
mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "돌": "견치돌", "쌓기": "골쌓기", "높이": 8, "초과증가율": 120},
)
def test_견본_파일_검사_통과(whole):
files = cm.load(SAMPLES)
assert len(files) == len(SAMPLES)
assert [x for name, data in files.items() for x in cm.check_form(name, data)] == []
# 9장 계수 표는 같은 절의 소요량 표와 함께 대조 — 소요량 파일 자체의 알림은 그 창 몫
with_09 = files | cm.load(["소요량_산림품셈_09장.json"])
# 절 통째를 출처로 둔 다른 창 표(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 cm.check_logics(files, whole) == []
def test_열쇠_겹침을_잡음():
"""다른 부문 표와 같은 열쇠면 먼저 읽힌 표만 찾기에 걸림."""
files = {
"계수_건설품셈_건축1장.json": {
"그룹": "계수",
"원문": "건설품셈",
"표": [{"열쇠": "1-1-1 계수"}],
},
"계수_건설품셈_기계설비1장.json": {
"그룹": "계수",
"원문": "건설품셈",
"표": [{"열쇠": "1-1-1 계수"}],
},
}
assert len(cm.duplicate_keys(files)) == 1
assert cm.duplicate_keys(cm.load()) == []
# 건설품셈 표·로직 열쇠 앞에 부문
one = {"그룹": "계수", "원문": "건설품셈", "판": "2026", "표": []}
got = cm.check_form("계수_건설품셈_건축1장.json", {**one, "표": [{"열쇠": "1-1-1 계수"}]})
assert any("부문 「건축」 없음" in x for x in got)
got = cm.check_form("계수_건설품셈_건축1장.json", {**one, "표": [{"열쇠": "건축 1-1-1 계수"}]})
assert not any("부문" in x for x in got)
def test_기계설비_강관_용접배관(whole):
"""1-1-2 관규격 15 · (배관공 3인 × 247,897 + 보통인부 1인 × 172,068) ÷ 83m · 기계경비 2%."""
got = mf.run(
whole,
"건설품셈:기계설비 1-1-2 강관 용접배관",
{"관규격": 15, "시공방법": "인력시공", "시공위치": "옥내일반배관"},
)
labor = (3 * Decimal(247897) + Decimal(172068)) / 83
assert cents(got["노무비"]) == cents(labor)
assert cents(got["경비"]) == cents(labor * Decimal("0.02"))
def test_기계설비_사각댐퍼_면적_가산(whole):
"""2-2-4 방화댐퍼 0.35㎡ · 0.1㎡ 초과 0.25㎡ = 증 3번 · 0.415 + 0.125×3 = 0.79인."""
got = mf.run(
whole, "건설품셈:기계설비 2-2-4 일반댐퍼(사각) 설치", {"댐퍼": "방화댐퍼", "댐퍼면적": 0.35}
)
assert got["줄"][0]["수량"] == Decimal("0.790")
def test_로직_검사가_틀린_열쇠를_잡음(whole):
row = {
"열쇠": "시험",
"입력": [{"이름": "지역", "고르기": ["전국평균", "없는곳"]}],
"중간": [{"이름": "x", "식": "찾기(계수:산림품셈:9-16-1 노체포설, 없는조건='K').적용"}],
"호표": [
{"종류": "재료", "요소": "재료:오피넷유가:경유:{지역}", "수량": "y", "비목": "재료비"}
],
"덧줄": [],
}
found, _ = mf.check_logic(whole, "산림품셈", row)
assert any("없는조건" in x for x in found)
assert any("경유:없는곳" in x for x in found)
assert any("「y」" in x for x in found)
def test_돌고_도는_참조():
assert mf.find_loops({"가": {"나"}, "나": {"가"}}) == [["가", "나", "가"]]
def test_찾기_값칸_있는_줄만():
table = {
"열쇠": "시험",
"조건": {"구분": "고르기", "높이": "고르기"},
"줄": [
{"구분": "비계공", "수량": 3},
{"높이": "4m이하", "시공량(m)": 100},
{"높이": "7m이하", "시공량(m)": 70},
],
}
assert mf.find(table, {"구분": "비계공"}, "수량")["수량"] == 3
assert mf.find(table, {"높이": "7m이하"}, "시공량(m)")["시공량(m)"] == 70
with pytest.raises(mf.FormulaError):
mf.find(table, {"높이": "7m이하"})
assert mf.parse("찾기(소요량:건설품셈:시험, 높이='7m이하').'시공량(m)'")[2] == "시공량(m)"
assert (
mf.parse("찾기(소요량:건설품셈:시험, '규준틀 높이'=높이).수량")[1][2][0][0] == "규준틀 높이"
)
assert (
mf.parse("찾기(소요량:건설품셈:5-1-1 P.P마대 쌓기, 구분=구분).수량")[1][1]
== "소요량:건설품셈:5-1-1 P.P마대 쌓기"
)
def test_범위규칙_이상이하():
table = {
"열쇠": "시험",
"조건": {"길이": "범위"},
"범위규칙": "이상·이하 · 겹친 누적 구간",
"줄": [
{"길이": [0, 10], "값": 1},
{"길이": [0, 20], "값": 2},
{"길이": [30, 40], "값": 3},
],
}
assert mf.find(table, {"길이": Decimal(10)})["값"] == 1
assert mf.find(table, {"길이": Decimal(15)})["값"] == 2
assert mf.find(table, {"길이": Decimal(30)})["값"] == 3
with pytest.raises(mf.FormulaError):
mf.find(table, {"길이": Decimal(25)})
def test_미공표_준용():
"""값 없는 직종에 준용 열쇠 — 그 직종 값으로 셈 · 호표 출처에 「준용: 이름」 · 준용 없으면 멈춤."""
files = cm.load()
lines = [
{
"종류": "인력",
"이름": "연마공",
"요소": "인력:건설노임:1032",
"수량": "2",
"비목": "노무비",
}
]
files["_시험"] = {"그룹": "로직", "원문": "시험", "줄": [{"열쇠": "준용", "호표": lines}]}
with pytest.raises(mf.FormulaError):
mf.run(mf.Master(files), "시험:준용", {})
row = next(r for r in files["인력_건설노임.json"]["줄"] if r["열쇠"] == "1032")
assert row["값"] is None and "준용" in row
row["준용"] = Decimal(1002) # M01 이 수로 적어도 열쇠로 읽음
got = mf.run(mf.Master(files), "시험:준용", {})
assert got["줄"][0]["출처"] == "준용: 보통인부"
assert got["노무비"] == 2 * Decimal(172068)