fix(B09): 분류 딱지 행에서 이름을 둘째 칸으로 읽음 — 자재·장비가 통째로 빠지던 자리

배분 창이 「초류종자살포 배합이 네 일위대가에 있나」를 물어 확인하다 **구멍을 찾음.**

- 품셈 표 중 **첫 칸이 분류 딱지**(「자재」·「장비」)이고 **이름이 둘째 칸**인 것이 있음:
  `['자재', '종      자', '', 'kg', '0.025']` · `['장비', '종자살포기', '2,500-3,000ℓ', …]`.
  첫 칸만 보고 읽어 **그 표의 자재·장비가 통째로 빠지고 있었음** — 씨앗뿜어붙이기에서
  종자·비료·피복제·침식안정제·색소·장비 3종이 다 빠지고 **보통인부 한 줄만** 남았음.
- 첫 칸이 분류 딱지면 이름을 둘째 칸에서 읽고, 값도 그 뒤 칸에서 찾게 고침.
- **매칭 106 → 116**(노무 100 → 110). 산출 파일 다시 냄.

**⚠ 그래도 배합은 아직 못 실림 — 원인은 사급 자재 단가 미결임**
- 종자·복합비료·화이버·합성접착제·색소가 **관급 카탈로그에 없음**(임도 자재가 관급에
  없다는 앞선 발견과 같은 자리). 장비 3종(종자살포기·트럭·물탱크)도 기종 카탈로그에 없음.
- **다만 이제 못 맞춘 목록에는 남음** — 구멍을 목록으로 드러내는 규칙 그대로.
  시험으로 못 박음(`test_seeding_materials_are_listed_not_dropped`).
- ⇒ 사면 4계열의 배합이 **B09 일위대가 몫**인 것은 맞으나, **사급 단가가 들어오기 전에는
  못 세움.** 「자재총괄에 이을 것이 없다」는 메인 판정은 유지되고, 그 자재는
  **㉡ 6번 슬롯 수동 입력 목록**으로 감(야면석·막자갈·고임돌·물구멍에 이어).

자체검증 — 신규 2건 포함 `pytest tmp/tests/ -q` **136 passed** · ruff 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 23:55:09 +09:00
co-authored by Claude Opus 5
parent 2faca7ac09
commit 991f7e31b1
3 changed files with 185 additions and 29 deletions
+14 -2
View File
@@ -41,6 +41,13 @@ _CATALOG_SUBPATH = ("resources", "data_cost_input_value")
_RE_SPEC = re.compile(r"[(]([^)]+)[)]|(\d+(?:\.\d+)?\s*(?:톤|ton|㎥|m3|㎡|㎜|mm|HP|kW))")
_RE_NUMBER = re.compile(r"^-?\d+(?:,\d{3})*(?:\.\d+)?$")
#: 첫 칸이 **분류 딱지**이고 이름이 둘째 칸에 오는 표가 있다.
#: 예 — `['자재', '종 자', '', 'kg', '0.025']` · `['장비', '종자살포기', …]`.
#: 이 표를 첫 칸만 보고 읽으면 **자재·장비가 통째로 빠진다**(2026-09-07 실측 —
#: 씨앗뿜어붙이기에서 종자·비료·피복제·침식안정제·색소·장비 3종이 다 빠지고
#: 보통인부 한 줄만 남았다).
_GROUP_LABELS = ("자재", "장비", "인력", "노무", "재료", "기계")
#: 표 머리글·소계 행의 첫 칸에 오는 말. 자원이 아니므로 `unmatched` 로도 안 올린다.
#: 이것을 안 거르면 못 맞춘 목록이 머리글로 가득 차 **쓸 수 없는 목록**이 된다.
#: ⚠ **부분일치로 보면 안 된다.** 「계」를 부분일치로 잡으면 `건설기계운전사`·`비계공`·
@@ -447,11 +454,16 @@ def match_table(
cells = [str(c) for c in row]
if not cells:
continue
# 첫 칸이 분류 딱지(「자재」·「장비」)면 **이름은 둘째 칸**이다.
name_cell = cells[0]
value_cells = cells[1:]
if _normalize(name_cell) in _GROUP_LABELS and len(cells) > 1:
name_cell = cells[1]
value_cells = cells[2:]
# 숫자 셀이 없는 행은 자원 줄이 아니다(제목·설명 행) — 목록에 안 올린다.
amount_cell = next(
(parse_amount(c) for c in cells[1:] if parse_amount(c) is not None), None
(parse_amount(c) for c in value_cells if parse_amount(c) is not None), None
)
if amount_cell is None:
continue
@@ -459,7 +471,7 @@ def match_table(
# ⚠ **카탈로그 조회를 먼저 한다.** 이름 필터를 앞에 두면 필터가 넓을 때
# 정상 자원이 조용히 사라진다(2026-09-07 실측 — 부분일치 필터가 매칭 14건을
# 지우고 있었음). 카탈로그에 있는 이름은 **정의상 자원**이다.
entry = _resolve_cell(catalog, name_cell, cells)
entry = _resolve_cell(catalog, name_cell, [name_cell, *value_cells])
if entry is None:
if is_non_resource_label(name_cell):
continue # 머리글·소계 — 못 맞춘 목록에도 안 올린다
@@ -211,6 +211,18 @@
"resource_spec": "",
"work_item_code": "FP-05-23-03"
},
{
"amount": "0.0007",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0135",
"raw_row_index": 8,
"resource_code": "1038",
"resource_kind": "labor",
"resource_name": "조경공",
"resource_spec": "",
"work_item_code": "FP-05-24-01"
},
{
"amount": "0.0004",
"amount_unit": "",
@@ -223,6 +235,18 @@
"resource_spec": "",
"work_item_code": "FP-05-24-01"
},
{
"amount": "0.0007",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0136",
"raw_row_index": 8,
"resource_code": "1038",
"resource_kind": "labor",
"resource_name": "조경공",
"resource_spec": "",
"work_item_code": "FP-05-24-02"
},
{
"amount": "0.0004",
"amount_unit": "",
@@ -235,6 +259,18 @@
"resource_spec": "",
"work_item_code": "FP-05-24-02"
},
{
"amount": "0.002",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0139",
"raw_row_index": 1,
"resource_code": "1038",
"resource_kind": "labor",
"resource_name": "조경공",
"resource_spec": "",
"work_item_code": "FP-05-25"
},
{
"amount": "0.0007",
"amount_unit": "",
@@ -415,6 +451,18 @@
"resource_spec": "",
"work_item_code": "FP-09-05-01"
},
{
"amount": "0.02",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0251",
"raw_row_index": 0,
"resource_code": "1012",
"resource_kind": "labor",
"resource_name": "용접공",
"resource_spec": "",
"work_item_code": "FP-09-08-01"
},
{
"amount": "0.08",
"amount_unit": "",
@@ -559,6 +607,30 @@
"resource_spec": "",
"work_item_code": "FP-09-13-18"
},
{
"amount": "0.019",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0290",
"raw_row_index": 0,
"resource_code": "1002",
"resource_kind": "labor",
"resource_name": "보통인부",
"resource_spec": "",
"work_item_code": "FP-09-19-02"
},
{
"amount": "0.0328",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0291",
"raw_row_index": 0,
"resource_code": "1002",
"resource_kind": "labor",
"resource_name": "보통인부",
"resource_spec": "",
"work_item_code": "FP-09-19-03"
},
{
"amount": "0.80",
"amount_unit": "",
@@ -943,6 +1015,18 @@
"resource_spec": "",
"work_item_code": "FP-12-18"
},
{
"amount": "0.034",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0368",
"raw_row_index": 1,
"resource_code": "1026",
"resource_kind": "labor",
"resource_name": "방수공",
"resource_spec": "",
"work_item_code": "FP-12-21"
},
{
"amount": "0.04",
"amount_unit": "",
@@ -955,6 +1039,18 @@
"resource_spec": "",
"work_item_code": "FP-12-21"
},
{
"amount": "0.003",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0370",
"raw_row_index": 2,
"resource_code": "1002",
"resource_kind": "labor",
"resource_name": "보통인부",
"resource_spec": "",
"work_item_code": "FP-12-23"
},
{
"amount": "4.0",
"amount_unit": "",
@@ -991,6 +1087,30 @@
"resource_spec": "",
"work_item_code": "FP-12-24-01"
},
{
"amount": "0.004",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0384",
"raw_row_index": 1,
"resource_code": "1002",
"resource_kind": "labor",
"resource_name": "보통인부",
"resource_spec": "",
"work_item_code": "FP-12-33"
},
{
"amount": "3.8",
"amount_unit": "",
"pum_form": "requirement",
"pum_table_id": "F0387",
"raw_row_index": 1,
"resource_code": "1008",
"resource_kind": "labor",
"resource_name": "철근공",
"resource_spec": "",
"work_item_code": "FP-12-34-03"
},
{
"amount": "2.2",
"amount_unit": "",
@@ -1288,12 +1408,12 @@
"sha256": "111208fd482dcfc3b8c8dd58004b153382fc46555d0d00985c19b45ebbc2e0dd"
},
"stats": {
"rows": 106,
"rows": 116,
"skipped_forms": {
"coefficient": 19,
"reference": 94,
"undetermined": 83
},
"unmatched": 344
"unmatched": 348
}
}
@@ -417,7 +417,7 @@
"work_item_code": "FP-05-22-04"
},
{
"cell": "자",
"cell": "자",
"pum_table_id": "F0135",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-01"
@@ -446,6 +446,12 @@
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-01"
},
{
"cell": "종자살포기",
"pum_table_id": "F0135",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-01"
},
{
"cell": "트 럭",
"pum_table_id": "F0135",
@@ -459,7 +465,7 @@
"work_item_code": "FP-05-24-01"
},
{
"cell": "자",
"cell": "자",
"pum_table_id": "F0136",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-02"
@@ -488,6 +494,12 @@
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-02"
},
{
"cell": "종자살포기",
"pum_table_id": "F0136",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-24-02"
},
{
"cell": "트 럭",
"pum_table_id": "F0136",
@@ -501,7 +513,7 @@
"work_item_code": "FP-05-24-02"
},
{
"cell": "자재",
"cell": "거 적",
"pum_table_id": "F0139",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-05-25"
@@ -999,7 +1011,7 @@
"work_item_code": "FP-09-04-01"
},
{
"cell": "자재",
"cell": "폭 약",
"pum_table_id": "F0243",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-05-01"
@@ -1016,6 +1028,18 @@
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-05-01"
},
{
"cell": "화 약 공",
"pum_table_id": "F0243",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-05-01"
},
{
"cell": "착 암 기",
"pum_table_id": "F0243",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-05-01"
},
{
"cell": "공기압축기",
"pum_table_id": "F0243",
@@ -1047,7 +1071,7 @@
"work_item_code": "FP-09-06-01"
},
{
"cell": "자재",
"cell": "아세틸렌",
"pum_table_id": "F0251",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-08-01"
@@ -1079,7 +1103,7 @@
{
"cell": "아스팔트",
"pum_table_id": "F0255",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음",
"work_item_code": "FP-09-10-02"
},
{
@@ -1478,6 +1502,18 @@
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-19-01"
},
{
"cell": "유압식백호우 (무한궤도,0.7㎥)",
"pum_table_id": "F0290",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-19-02"
},
{
"cell": "공기압축기(3.5㎥/min)",
"pum_table_id": "F0291",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-09-19-03"
},
{
"cell": "소형브레이커",
"pum_table_id": "F0291",
@@ -1601,19 +1637,19 @@
{
"cell": "각 재",
"pum_table_id": "F0336",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음",
"work_item_code": "FP-12-04"
},
{
"cell": "철 선",
"pum_table_id": "F0336",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음",
"work_item_code": "FP-12-04"
},
{
"cell": "박 리 제",
"pum_table_id": "F0336",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음",
"work_item_code": "FP-12-04"
},
{
@@ -1737,13 +1773,13 @@
"work_item_code": "FP-12-18"
},
{
"cell": "자재",
"cell": "아스팔트(㏊-500)",
"pum_table_id": "F0368",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음",
"work_item_code": "FP-12-21"
},
{
"cell": "자재",
"cell": "부 직 포",
"pum_table_id": "F0370",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-12-23"
@@ -1796,12 +1832,6 @@
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-12-24-01"
},
{
"cell": "자재",
"pum_table_id": "F0376",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-12-27-02"
},
{
"cell": "접착제",
"pum_table_id": "F0376",
@@ -1815,13 +1845,7 @@
"work_item_code": "FP-12-27-02"
},
{
"cell": "재료",
"pum_table_id": "F0384",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-12-33"
},
{
"cell": "자재",
"cell": "결속선(R-0.9mm)",
"pum_table_id": "F0387",
"reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)",
"work_item_code": "FP-12-34-03"