Files
Aislo/resources/tester/test_build_mach.py
T
eomsangdonandClaude Opus 5 83220dc231 feat(b08): 건설 표 모양 판정층 — 미판정 891 → 276 · 건설 밑수 미판독 184 → 90 · 산림 0 그대로
브레인 일감. 표 모양 판정이 산림 표에 맞춰져 있어 건설 표를 못 가렸음(건설 미판정 891 · 산림 0).

- `B08_Quantity_Build_WorkItemMaster_FormsConst.py` — **산림 잣대가 미판정으로 떨어질 때만** 도는 건설 층
  · 기계 손료표(머리 「분류번호」+「상각 비율」) 122 · 율·계수표(손율·요율·보정계수) · 값 없는 설명·서식표 ·
    건설 직종(특급·고급·중급·초급 기술자 · 지적기사 · 비계공 …) · 줄에 단위 칸+값 · 값 자리 계산식 ·
    줄 이름이 공식 기호뿐(β·E) · 값이 모두 백분율
  · ⚠ 비고 칸의 산출식은 값으로 안 봄 — 인력품 표가 계수로 뒤집히던 자리(9-5-4 수치지도)
  · ⚠ 꼬리 단위는 품·자재 단위만 값으로 셈 — `24hr`·`30톤`·`0.166일` 은 규격·기간 표기
  · ⚠ **못 가리면 미판정으로 남김**(276) — 지어내 채우지 않음
- 밑수 읽기 넓힘(`_Table.SOURCE_BASIS_RE`) — 건설은 절 제목 끝에 밑수를 적음
  · `(용접개소당)`·`(세면기 개당)`·`(절단길이 10m당)`·`(m당 : 관길이기준)`·`(일당)`·`(기당)` 을 읽음
  · 단위에 일·기·조·명·틀 더함 · ⚠ 「회」는 넣지 않음(2026-09-17 서브 판정)
  · ⚠ `(1ha당, 100본당)` 처럼 **밑수가 둘이면 안 고름** · `공㎥당` 이 `㎥` 로 깎이던 것 고침
  · ⚠ **작업조 표에는 「일」 밑수를 안 붙임**(c3da333e — 붙이면 유로폼 호표가 사라짐)
  · 건설 밑수 확보 537 → 639 · 미판독 184 → 90 · 산림 확보 251 → 253(원문에 있던 2-2-4 ha당 등)
- 브레인 ①[주] 17 중 3 — `md_notes` 이어짐에 「- 4. 고철공제 : A=…」 꼴(콜론·식) 과 중첩 [주] 를 더함
  · 남은 14 는 마스터가 뒤 표의 [주]까지 붙여 둔 자리 — 「애매하면 안 깎는 쪽」으로 까닭만 달아 둠
- 브레인 ②밑수 5 중 3 — `(단위 : ㎥)`(당 없음) · 줄 칸의 `(160ha당)` · 설명 글이 앞에 붙은 `(…, ha당)` 읽음
  · F0322·F0323 은 원문 밑수가 표 둘 아래 산출식에만 있어 사람 판정 유지
-  금액 불변 — 직접공사비 5,983,724 · 합계 10,124,000 그대로
- `test_form_const.py` 22 — 산림이 건설 층에 안 닿음 · 꼴 여섯 가름 · 못 가리면 미판정 · 밑수 아홉 꼴
- 곁가지(받은 md 손질이 부른 것)
  · 8-11 이동식 임목파쇄기 — 랩탑 서브가 8-4 를 줄마다 풀자 **연료 칸이 「-」 인 빈 레코드**가 생겨 산림
    8-11 [주]⑤(13.5ℓ·16%) 층이 통째로 빠졌음 → 「연료가 비면 산림 [주]가 정본」으로 두 자리 고침
  · 기계 벌 — 중유 기종(`중유487.2`) 을 경유로 적던 것 · 연료가 둘인 칸(`73.7+휘발유54.5`) 을 반쪽만 싣던 것 ·
    원문에 없는 기종을 옛 벌에서 이어받던 것 고침(연료 274)
  · ⚠ 이어받기는 **원문에 줄이 있는데 연료를 못 읽은 코드**까지 — 못 읽은 줄 + 주연료 칸이 수가 아닌 줄
    (양수기 7993-0020 은 원문이 규격을 연료 칸에 밀어 적음 `1.49㎾` · 사람이 맞춘 1.3ℓ 를 지킴)
  · 이름표 줄 수·건설 공종 축 셈·열쇠 지문 갱신(까닭을 파일에 적음)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-18 09:07:49 +09:00

164 lines
7.3 KiB
Python

"""기계 원천 벌 뽑기(`_build_mach.py`) 시험 — 2026-09-18 브레인 일감(랩탑 메인).
못박는 것
- 기계 자료가 **지금 8장 md 와 같음** — md 가 고쳐졌는데 안 뽑았으면 빨강(뽑기를 돌리라는 뜻).
- ⚠ **줄이 밀려도 값이 그대로** — 자리는 줄 번호가 아니라 **기계 코드**로 잡음.
- 한 칸에 뭉친 줄(`0101-0007 0010 …`)과 줄마다 푼 판이 **같은 값**을 냄(코덱스 8-4 손질이 그 꼴).
- 토막 수가 안 맞으면 **지어내지 않고** `parse_audit` 에 적음.
- 사람이 맞춘 줄(`parse_method`)·손질 사유(`note`)는 **옛 벌에서 이어받음** — ⚠ 원문에 줄이 있는 것만.
금액 불변 — 뽑은 뒤 `test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724)이 잼.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources" / "data_cost_input_value"))
import _build_mach as build # noqa: E402
GLUED = """
8-3 기계손료(0101) 불도저(무한궤도)
| 분류 번호 | 규격 (ton) | 내용 시간 | 연간표준 가동시간 | 상각 비율 | 정비 비율 | 연간 관리 비율 | 시 간 당(10-7) | | | |
|---|---|---|---|---|---|---|---|---|---|---|
| | | | | | | | 상각비 계수 | 정비비 계수 | 관리비 계수 | 계 |
| 0101-0007 | 7 | 12,000 | 1,250 | 0.9 | 0.7 | 0.1 | 750 | 583 | 478 | 1,811 |
| 0010 | 10 | 12,000 | 1,250 | 0.9 | 0.7 | 0.1 | 750 | 583 | 478 | 1,811 |
8-4 운전경비 산정8-4-1 [00]토공기계
| 분류번호 | 기계명 | 규격 | 주연료 (ℓ/hr) | 잡재료 (주연료의%) | 조종원 (인/일) |
|---|---|---|---|---|---|
| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |
8-5 기계가격8-5-1 [00]토공기계
| 기 종 | 분류번호 | 가격(₩) |
|---|---|---|
| 불 도 저 ( 무 한 궤 도 ) | 0101-0007 0010 | 76,547 168,668 |
"""
UNGLUED = GLUED.replace(
"| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |",
"| 0101-0007 | 불도저(무한궤도) | 7ton | 9.0 | 16% | 1 |\n| 0010 | | 10 | 12.5 | 16 | 1 |",
)
BARE = {
"sources": [{"path": "x.md", "sha256": "", "role": "primary"}],
"variables": {
"mach_price": {"unit": "thousand_KRW", "key": "machine_code", "records": []},
"mach_loss_coef": {"unit": "1/hour", "key": "machine_code", "records": []},
"mach_fuel_rate": {"unit": "L/hour", "key": "machine_code", "parsed_records": [], "source_tables": []},
},
} # fmt: skip
def _of(doc: dict, key: str, field: str = "records") -> dict:
return {r["machine_code"]: r for r in doc["variables"][key][field]}
def _extract(text: str, old: dict | None = None) -> dict:
return build.extract(json.loads(json.dumps(old or BARE)), text.encode("utf-8"))[0]
def test_기계_자료가_지금_md_와_같음() -> None:
"""md 가 고쳐졌으면 `_build_mach.py` 를 돌릴 것."""
old = json.loads(build.TARGET.read_text(encoding="utf-8"))
doc, report = build.extract(old, (ROOT / old["sources"][0]["path"]).read_bytes())
diff = build.diff_report(old, doc)
stale = {k: {n: len(v) for n, v in item.items() if v} for k, item in diff.items() if any(item.values())} # fmt: skip
assert not stale, f"기계 자료가 md 와 다름 — _build_mach.py 돌릴 것: {stale}"
def test_줄이_밀려도_값이_그대로() -> None:
doc = _extract(GLUED)
shifted = _extract(GLUED.replace("8-4 운전경비", "보탠 줄\n\n보탠 줄\n\n8-4 운전경비"))
for key, field in (("mach_price", "records"), ("mach_loss_coef", "records"), ("mach_fuel_rate", "parsed_records")): # fmt: skip
assert _of(doc, key, field) == _of(shifted, key, field)
def test_뭉친_줄과_풀린_줄이_같은_값() -> None:
"""코덱스가 8-4 를 줄마다 풀어 놓아도 값이 같아야 함 — 이름만 원문 표기를 따름."""
glued, unglued = _of(_extract(GLUED), "mach_fuel_rate", "parsed_records"), _of(_extract(UNGLUED), "mach_fuel_rate", "parsed_records") # fmt: skip
assert set(glued) == set(unglued) == {"0101-0007", "0101-0010"}
for code in glued:
assert {k: v for k, v in glued[code].items() if k != "machine_name"} == {
k: v for k, v in unglued[code].items() if k != "machine_name"
}
assert glued["0101-0010"]["fuel_rate_l_per_hour"] == 12.5
def test_손료_2단_머리를_읽고_계수를_시간당으로() -> None:
loss = _of(_extract(GLUED), "mach_loss_coef")["0101-0007"]
assert loss["source_coefficient_1e_minus_7"] == 1811
assert loss["loss_coefficient_per_hour"] == 1811 * 1e-7
assert (loss["economic_life_hours"], loss["depreciation_ratio"], loss["specification"]) == (
12000,
0.9,
"7",
)
assert loss["machine_name"] == "불도저(무한궤도)"
def test_토막_수가_안_맞으면_지어내지_않고_적어_둠() -> None:
broken = GLUED.replace("| 9.0 12.5 |", "| 9.0 |")
doc, report = build.extract(json.loads(json.dumps(BARE)), broken.encode("utf-8"))
assert "0101-0007" not in _of(doc, "mach_fuel_rate", "parsed_records")
assert doc["parse_audit"]["unparsed_fuel_rows"] == 1 # 자료엔 셈만
assert report["unparsed_rows"]["fuel"] == [
{"what": "0101-0007 0010", "codes": 2}
] # 자세한 것은 알림에
def test_사람이_맞춘_줄과_사유는_이어받음() -> None:
"""원문에 줄은 있으나 연료를 못 읽은 자리(`1.49㎾` — 칸 밀림)만 이어받음.
⚠ 원문에 아예 없는 기종은 안 이어받음 — 옛 벌에만 있는 줄이 자료에 남으면 안 됨.
"""
md = GLUED.replace(
"| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |",
"| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |\n"
"| 7993-0020 | 양수기 | 1.49㎾ | 1.49㎾ | - | - |",
)
old = json.loads(json.dumps(BARE))
old["variables"]["mach_fuel_rate"]["parsed_records"] = [
{
"machine_code": "7993-0020",
"machine_name": "양수기",
"fuel_type": "gasoline",
"fuel_rate_l_per_hour": 1.3,
"parse_method": "verified_source_row_alignment",
},
{
"machine_code": "9999-0001",
"machine_name": "원문에 없는 기계",
"fuel_type": "diesel",
"fuel_rate_l_per_hour": 4.3,
"parse_method": "verified_source_row_alignment",
},
] # fmt: skip
old["variables"]["mach_loss_coef"]["records"] = [
{
"machine_code": "0101-0007",
"machine_name": "불도저(무한궤도)",
"note": "칸 밀림 바로잡음",
}
]
doc = _extract(md, old)
fuel = _of(doc, "mach_fuel_rate", "parsed_records")
assert fuel["7993-0020"]["parse_method"] == "verified_source_row_alignment"
assert fuel["7993-0020"]["fuel_rate_l_per_hour"] == 1.3
assert "9999-0001" not in fuel
assert _of(doc, "mach_loss_coef")["0101-0007"]["note"] == "칸 밀림 바로잡음"
def test_사라진_코드는_알림에() -> None:
old = json.loads(json.dumps(BARE))
old["variables"]["mach_price"]["records"] = [
{"machine_code": "9999-0001", "machine_name": "옛 기계", "price_thousand_krw": 1}
]
doc = _extract(GLUED, old)
assert build.diff_report(old, doc)["mach_price"]["사라진 코드"] == ["9999-0001"]