From 34d90a14d1bd7e534e219b85c7f31284b3e67e48 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 20:43:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=EA=B8=B0=EA=B3=84=EA=B2=BD?= =?UTF-8?q?=EB=B9=84(S=E2=86=92X)=20+=20=EA=B8=B0=EC=A2=85=20=EC=B9=B4?= =?UTF-8?q?=ED=83=88=EB=A1=9C=EA=B7=B8=20=EB=A7=A4=EC=B9=AD=20(PLAN=209-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **`B09_Estimation_MachineCost.py` 신설 — 취득가 → 시간당 사용료 두 단계** - `mach_base_2026.json` 실사: `mach_price` **613 기종**(`machine_code`+`specification`), `mach_loss_coef` 387건(시간당 손료계수·내용시간·연간표준시간). - **시간당 손료 = 취득가 × 손료계수.** 취득가가 **천원 단위**라 환산을 놓치면 1,000배 틀리는 자리 — 테스트로 박음. 굴착기(무한궤도) 0.7㎥ → **24,554원/hr** (STC 2024 관측 23,128 과 같은 자릿수, 취득가 판 차이로 설명됨). - ⚠ **빠진 성분을 0 으로 안 때움** — `mach_fuel_rate`·`mach_operator_map` 이 **0건**이라 연료(재료비)·운전사(노무비)를 못 채움. `HourlyMachineCost.gaps` 로 **공백을 알림**. 실측 비중이 노무 53·재료 20·경비 27 % 라 손료만 쓰면 **4분의 1**이 됨 — 조용히 총액에 섞이면 안 되는 자리. - 손료계수 없는 기종 **230건**도 0 으로 안 때우고 멈춤. **자원 축에 기종 매칭 추가** - 기종 셀은 괄호 안에 **형식과 규격이 섞여** 있음 — 「굴착기(무한궤도, 0.7㎥)」는 이름 `굴착기(무한궤도)` + 규격 `0.7`. 형식은 이름의 일부이고 숫자 조각만 규격임. - **규격이 옆 칸에 있는 표**가 많아(`['굴착기 (무한궤도)','굴착기(무한궤도,0.2㎥)','hr',…]) 뒷 칸에서 규격 후보를 찾아 고름. 규격 없이 매칭 성공으로 치지 않는 규칙은 그대로. - 산출 갱신 — 매칭 86 → **91행**(노무 86 + 기종 5), 미매칭 274 → **269건**. 자체검증 — 신규 7건 포함 `pytest tmp/tests/ -q` **104 passed** · ruff 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_MachineCost.py | 185 ++++++++++++++++++ B09_Estimation/B09_Estimation_ResourceAxis.py | 109 ++++++++++- .../resource_axis_2026-01-01.json | 114 ++++++++--- .../unmatched_2026-01-01.json | 32 +-- 4 files changed, 379 insertions(+), 61 deletions(-) create mode 100644 B09_Estimation/B09_Estimation_MachineCost.py diff --git a/B09_Estimation/B09_Estimation_MachineCost.py b/B09_Estimation/B09_Estimation_MachineCost.py new file mode 100644 index 00000000..81b0dca1 --- /dev/null +++ b/B09_Estimation/B09_Estimation_MachineCost.py @@ -0,0 +1,185 @@ +"""B09 원가계산 — 기계경비 (PLAN 9-3 의 `S` → `X` 두 단계). + +실무·교본이 같은 두 단계다 (신규 문서 5장 라-1 예제도 같은 모양): + + S 취득가(천원) ──(손료계수)──► X 시간당 중기사용료 + = 손료(경비) + 연료(재료) + 운전사(노무) + +이 모듈이 내는 것은 **시간당 사용료 한 시간분**이고, 3분할(재료·노무·경비)로 낸다. +`B09_Estimation_PriceBook` 의 `MACHINE_BASE`(S) · `MACHINE_HOURLY`(X) 층에 그대로 앉는다. + +데이터 (`resources/data_cost_input_value/mach_base_2026.json`) + - `mach_price` **613 기종** — `machine_code` + `specification` 으로 규격이 갈린다. + - `mach_loss_coef` **387건** — 시간당 손료계수·내용시간·연간표준시간. + - ⚠ `mach_fuel_rate` **0건** · `mach_operator_map` **0건** — **비어 있다.** + 연료소모량과 기종별 운전사 직종은 품셈 본문에 있고, 아직 뽑히지 않았다. + +⚠ **그래서 이 모듈은 손료(경비)까지만 채우고, 연료·운전사는 「공백」으로 표시한다.** +0 으로 때우면 **시간당 사용료가 절반 이하로 나오고 그대로 총액에 섞인다** — 실측 +비중이 노무 53 % · 재료 20 % · 경비 27 % 라 손료만으로는 4분의 1 남짓이다. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_PriceBook import Money3 + +_CATALOG_SUBPATH = ("resources", "data_cost_input_value") +_THOUSAND = Decimal(1000) + + +class MachineCostError(LookupError): + """기계경비를 세울 수 없는 경우. 0 으로 때우지 않고 멈춘다.""" + + +def _project_root() -> str: + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _read_json(file_name: str) -> dict[str, Any]: + with open(os.path.join(_project_root(), *_CATALOG_SUBPATH, file_name), encoding="utf-8") as h: + return json.load(h) + + +@dataclass(frozen=True) +class MachineSpec: + """기종 한 줄 — 규격까지 붙어야 한 대가 정해진다.""" + + machine_code: str + name: str + specification: str + price_thousand_krw: Decimal + loss_coefficient_per_hour: Decimal | None = None + economic_life_hours: int | None = None + annual_standard_hours: int | None = None + + @property + def display_name(self) -> str: + return f"{self.name} {self.specification}".strip() + + +@dataclass +class MachineCatalog: + """613 기종. **이름만으로는 못 고른다** — 규격이 있어야 한 대가 정해진다.""" + + machines: dict[str, MachineSpec] = field(default_factory=dict) + + def by_name(self, name: str) -> list[MachineSpec]: + return [m for m in self.machines.values() if m.name == name] + + def resolve(self, name: str, specification: str) -> MachineSpec | None: + """이름 + 규격으로 한 대를 고른다. 규격이 없으면 **고르지 않는다**.""" + found = self.by_name(name) + if not found: + return None + if len(found) == 1 and not specification: + return found[0] + narrowed = [m for m in found if m.specification == specification] + return narrowed[0] if len(narrowed) == 1 else None + + def get(self, machine_code: str) -> MachineSpec: + try: + return self.machines[machine_code] + except KeyError as exc: + raise MachineCostError(f"기종 카탈로그에 없는 코드입니다: {machine_code}") from exc + + +def load_machine_catalog(file_name: str = "mach_base_2026.json") -> MachineCatalog: + """취득가 613건에 손료계수 387건을 붙여 카탈로그 한 벌을 만든다.""" + variables = _read_json(file_name)["variables"] + coefficients = { + r["machine_code"]: r for r in variables.get("mach_loss_coef", {}).get("records", []) + } + + catalog = MachineCatalog() + for row in variables.get("mach_price", {}).get("records", []): + code = row["machine_code"] + coefficient = coefficients.get(code, {}) + catalog.machines[code] = MachineSpec( + machine_code=code, + name=row["machine_name"], + specification=str(row.get("specification", "")), + price_thousand_krw=Decimal(str(row["price_thousand_krw"])), + loss_coefficient_per_hour=( + Decimal(str(coefficient["loss_coefficient_per_hour"])) + if "loss_coefficient_per_hour" in coefficient + else None + ), + economic_life_hours=coefficient.get("economic_life_hours"), + annual_standard_hours=coefficient.get("annual_standard_hours"), + ) + return catalog + + +def hourly_loss_cost(machine: MachineSpec) -> Decimal: + """시간당 손료 = 취득가 × 손료계수. + + 취득가가 **천원 단위**라 원으로 환산한다 — 이 단위를 놓치면 1,000배 틀린다. + """ + if machine.loss_coefficient_per_hour is None: + raise MachineCostError( + f"{machine.display_name}: 손료계수가 없습니다 (취득가만 있는 기종 226건 중 하나)" + ) + return machine.price_thousand_krw * _THOUSAND * machine.loss_coefficient_per_hour + + +@dataclass +class HourlyMachineCost: + """시간당 중기사용료 — 3분할과 **채우지 못한 성분 목록**을 함께 낸다.""" + + machine: MachineSpec + money: Money3 + gaps: list[str] = field(default_factory=list) + + @property + def is_complete(self) -> bool: + return not self.gaps + + +def hourly_machine_cost( + machine: MachineSpec, + *, + fuel_liters_per_hour: Decimal | None = None, + fuel_price_per_liter: Decimal | None = None, + operator_daily_wage: Decimal | None = None, + operator_hours_per_day: int = 8, +) -> HourlyMachineCost: + """시간당 사용료 한 시간분. + + 손료(경비)는 카탈로그로 바로 나온다. **연료(재료)·운전사(노무)는 값을 주지 않으면 + 0 으로 때우지 않고 `gaps` 에 적어 돌려준다** — 빠진 채로 총액에 섞이는 것이 + 이 자리에서 제일 위험하다. + """ + gaps: list[str] = [] + expense = hourly_loss_cost(machine) + + material = Decimal(0) + if fuel_liters_per_hour is None or fuel_price_per_liter is None: + # TODO(미결 PLAN 9-6): `mach_fuel_rate` 0건 — 연료소모량이 품셈 본문에만 있다. + gaps.append("연료소모량(L/hr) 미확보 — 재료비 성분 비어 있음") + else: + material = fuel_liters_per_hour * fuel_price_per_liter + + labor = Decimal(0) + if operator_daily_wage is None: + # TODO(미결 PLAN 9-6): `mach_operator_map` 0건 — 기종별 운전사 직종이 품셈 본문에만 있다. + gaps.append("운전사 직종 매핑 미확보 — 노무비 성분 비어 있음") + else: + labor = operator_daily_wage / Decimal(operator_hours_per_day) + + return HourlyMachineCost( + machine=machine, + money=Money3(material=material, labor=labor, expense=expense), + gaps=gaps, + ) + + +def catalog_gaps(catalog: MachineCatalog) -> dict[str, int]: + """카탈로그 자체의 공백 — 몇 기종이 손료계수를 못 가졌나.""" + missing = [m for m in catalog.machines.values() if m.loss_coefficient_per_hour is None] + return {"machines": len(catalog.machines), "without_loss_coefficient": len(missing)} diff --git a/B09_Estimation/B09_Estimation_ResourceAxis.py b/B09_Estimation/B09_Estimation_ResourceAxis.py index d121d20b..ab7c6d58 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis.py @@ -169,6 +169,30 @@ def load_labor_catalog(file_name: str = "labor_const_2026-01-01.json") -> Resour return ResourceCatalog(entries=entries, aliases=dict(variables.get("aliases", {}))) +def load_machine_catalog_entries(file_name: str = "mach_base_2026.json") -> list[CatalogEntry]: + """기종 카탈로그 613건을 매칭용 항목으로 편다. + + ⚠ **규격이 매칭의 일부**다 — 「굴착기(무한궤도)」만 23 규격이라 이름만으로는 + 한 대가 안 정해진다(지시 4번). `CatalogEntry.spec` 에 규격을 실어 둔다. + """ + from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog + + catalog = load_machine_catalog(file_name) + return [ + CatalogEntry(code=m.machine_code, name=m.name, kind="machine", spec=m.specification) + for m in catalog.machines.values() + ] + + +def load_combined_catalog() -> ResourceCatalog: + """노임 + 기종을 한 벌로. 자재는 카탈로그가 아직 없다.""" + labor = load_labor_catalog() + return ResourceCatalog( + entries=[*labor.entries, *load_machine_catalog_entries()], + aliases=labor.aliases, + ) + + def load_work_item_master( file_name: str = "work_item_master_2026-01-01.json", ) -> dict[str, Any]: @@ -191,6 +215,57 @@ def split_name_and_spec(cell: str) -> tuple[str, str]: return name, spec +#: 기종 이름의 괄호 안은 **규격과 형식이 섞여** 있다 — +#: 「굴착기(무한궤도, 0.7㎥)」 는 이름 `굴착기(무한궤도)` + 규격 `0.7` 이다. +#: 형식(무한궤도·타이어)은 이름의 일부이고, 숫자가 든 조각만 규격이다. +_RE_MACHINE = re.compile(r"^(?P[^()()]+)[((](?P[^))]*)[))]") +_RE_SIZE_TOKEN = re.compile(r"\d+(?:\.\d+)?") + + +def parse_machine_cell(cell: str) -> tuple[str, str]: + """기종 셀을 카탈로그 이름과 규격으로 가른다. + + 「굴착기(무한궤도, 0.7㎥)」 → (`굴착기(무한궤도)`, `0.7`) + 「굴착기 (무한궤도)」 → (`굴착기(무한궤도)`, ``) ← 규격은 옆 칸에 있다 + 괄호가 없으면 원문 그대로 돌려준다. + """ + text = _normalize(cell) + match = _RE_MACHINE.match(text) + if not match: + return text, "" + base = match.group("base") + parts = [p for p in re.split(r"[,·/]", match.group("inner")) if p] + form_parts = [p for p in parts if not _RE_SIZE_TOKEN.search(p)] + size_parts = [p for p in parts if _RE_SIZE_TOKEN.search(p)] + name = f"{base}({','.join(form_parts)})" if form_parts else base + spec = "" + if size_parts: + found = _RE_SIZE_TOKEN.search(size_parts[0]) + spec = found.group(0) if found else "" + return name, spec + + +def spec_candidates(cells: list[str]) -> list[str]: + """규격이 옆 칸에 있는 표가 많다 — 뒷 칸들에서 규격 후보를 모은다. + + 실측 배치: `['굴착기+부착용집게', '0.2㎥', 'hr', '2.71', …]` · + `['굴착기 (무한궤도)', '굴착기(무한궤도,0.2㎥)', 'hr', '0.80', …]` + """ + found: list[str] = [] + for cell in cells: + text = _normalize(cell) + if not text or len(text) > 30: + continue + _, spec = parse_machine_cell(text) + if spec: + found.append(spec) + continue + token = _RE_SIZE_TOKEN.fullmatch(text.rstrip("㎥㎡톤tonm³")) + if token: + found.append(token.group(0)) + return found + + def parse_amount(cell: str) -> Decimal | None: """숫자 셀만 값으로 본다. 숫자가 아니면 None — 억지로 읽지 않는다.""" text = _normalize(cell) @@ -276,6 +351,34 @@ class AxisResult: skipped_forms: dict[str, int] = field(default_factory=dict) +def _resolve_cell(catalog: ResourceCatalog, name_cell: str, cells: list[str]): + """셀 하나를 카탈로그 한 줄로 푼다 — 세 가지 모양을 차례로 시도한다. + + ① 셀 전체가 곧 이름 (「보통인부」) + ② 기종 셀 (「굴착기(무한궤도, 0.7㎥)」 → 이름 + 규격) + ③ 규격이 **옆 칸**에 있는 표 (「굴착기 (무한궤도)」 | 「0.7㎥」) + """ + machine_name, machine_spec = parse_machine_cell(name_cell) + plain_name, plain_spec = split_name_and_spec(name_cell) + + for name, spec in ((machine_name, machine_spec), (plain_name, plain_spec)): + if not name: + continue + entry = catalog.resolve(name, spec) + if entry is not None: + return entry + + # 이름은 맞는데 규격이 없어 못 고른 경우 — 옆 칸에서 규격을 찾는다. + for name in (machine_name, plain_name): + if len(catalog.by_name(name)) <= 1: + continue + for candidate in spec_candidates(cells[1:]): + entry = catalog.resolve(name, candidate) + if entry is not None: + return entry + return None + + def match_table( node: dict[str, Any], table: dict[str, Any], @@ -307,9 +410,9 @@ def match_table( if amount_cell is None: continue - name, spec = split_name_and_spec(name_cell) - entry = catalog.resolve(name, spec) + entry = _resolve_cell(catalog, name_cell, cells) if entry is None: + name, spec = split_name_and_spec(name_cell) found = catalog.by_name(name) if len(found) > 1: reason = "규격이 없어 같은 이름 여럿 중 고를 수 없음" @@ -338,7 +441,7 @@ def match_table( resource_kind=entry.kind, resource_code=entry.code, resource_name=entry.name, - resource_spec=entry.spec or spec, + resource_spec=entry.spec, amount=amount, amount_unit=unit, raw_row_index=index, diff --git a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json index f6c2704f..48e47176 100644 --- a/resources/data_cost_resource_axis/resource_axis_2026-01-01.json +++ b/resources/data_cost_resource_axis/resource_axis_2026-01-01.json @@ -112,7 +112,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-05-11" }, { @@ -304,7 +304,7 @@ "resource_code": "1003", "resource_kind": "labor", "resource_name": "특별인부", - "resource_spec": "체인톱 사용", + "resource_spec": "", "work_item_code": "FP-06-05" }, { @@ -364,7 +364,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-03-01" }, { @@ -412,7 +412,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-12-02" }, { @@ -424,7 +424,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-12-03" }, { @@ -436,7 +436,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-07" }, { @@ -448,7 +448,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-08" }, { @@ -460,7 +460,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-09" }, { @@ -472,7 +472,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-13" }, { @@ -484,7 +484,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-14" }, { @@ -496,7 +496,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-15" }, { @@ -508,7 +508,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-16" }, { @@ -520,7 +520,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-17" }, { @@ -532,9 +532,21 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-09-13-18" }, + { + "amount": "0.80", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0294", + "raw_row_index": 0, + "resource_code": "0201-0020", + "resource_kind": "machine", + "resource_name": "굴착기(무한궤도)", + "resource_spec": "0.2", + "work_item_code": "FP-09-21" + }, { "amount": "0.03", "amount_unit": "", @@ -547,6 +559,18 @@ "resource_spec": "", "work_item_code": "FP-09-21" }, + { + "amount": "0.46", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0294", + "raw_row_index": 2, + "resource_code": "0201-0070", + "resource_kind": "machine", + "resource_name": "굴착기(무한궤도)", + "resource_spec": "0.7", + "work_item_code": "FP-09-21" + }, { "amount": "0.03", "amount_unit": "", @@ -568,7 +592,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-06-02" }, { @@ -580,7 +604,7 @@ "resource_code": "1003", "resource_kind": "labor", "resource_name": "특별인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-01" }, { @@ -592,7 +616,7 @@ "resource_code": "1003", "resource_kind": "labor", "resource_name": "특별인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-02" }, { @@ -604,7 +628,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-02" }, { @@ -616,7 +640,7 @@ "resource_code": "1003", "resource_kind": "labor", "resource_name": "특별인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-03" }, { @@ -628,7 +652,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-03" }, { @@ -640,7 +664,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-10-07-04" }, { @@ -844,7 +868,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-12-24-02" }, { @@ -856,7 +880,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-12-26" }, { @@ -868,7 +892,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-12-24-01" }, { @@ -904,7 +928,7 @@ "resource_code": "1002", "resource_kind": "labor", "resource_name": "보통인부", - "resource_spec": "인", + "resource_spec": "", "work_item_code": "FP-13-02-01" }, { @@ -943,6 +967,18 @@ "resource_spec": "", "work_item_code": "FP-13-06-02" }, + { + "amount": "4.80", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0420", + "raw_row_index": 4, + "resource_code": "0201-0080", + "resource_kind": "machine", + "resource_name": "굴착기(무한궤도)", + "resource_spec": "0.8", + "work_item_code": "FP-13-06-02" + }, { "amount": "1.86", "amount_unit": "", @@ -955,6 +991,18 @@ "resource_spec": "", "work_item_code": "FP-13-06-03" }, + { + "amount": "6.86", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0421", + "raw_row_index": 4, + "resource_code": "0201-0080", + "resource_kind": "machine", + "resource_name": "굴착기(무한궤도)", + "resource_spec": "0.8", + "work_item_code": "FP-13-06-03" + }, { "amount": "0.58", "amount_unit": "", @@ -967,6 +1015,18 @@ "resource_spec": "", "work_item_code": "FP-13-07-01" }, + { + "amount": "2.40", + "amount_unit": "", + "pum_form": "requirement", + "pum_table_id": "F0422", + "raw_row_index": 4, + "resource_code": "0201-0080", + "resource_kind": "machine", + "resource_name": "굴착기(무한궤도)", + "resource_spec": "0.8", + "work_item_code": "FP-13-07-01" + }, { "amount": "1.01", "amount_unit": "", @@ -1048,12 +1108,12 @@ "sha256": "111208fd482dcfc3b8c8dd58004b153382fc46555d0d00985c19b45ebbc2e0dd" }, "stats": { - "rows": 86, + "rows": 91, "skipped_forms": { "coefficient": 19, "reference": 94, "undetermined": 83 }, - "unmatched": 274 + "unmatched": 269 } } \ No newline at end of file diff --git a/resources/data_cost_resource_axis/unmatched_2026-01-01.json b/resources/data_cost_resource_axis/unmatched_2026-01-01.json index 06884b2a..d8aded08 100644 --- a/resources/data_cost_resource_axis/unmatched_2026-01-01.json +++ b/resources/data_cost_resource_axis/unmatched_2026-01-01.json @@ -1112,18 +1112,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-09-19-03" }, - { - "cell": "굴착기 (무한궤도)", - "pum_table_id": "F0294", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-21" - }, - { - "cell": "굴착기(무한궤도,0.7㎥)", - "pum_table_id": "F0294", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-09-21" - }, { "cell": "콘크리트", "pum_table_id": "F0313", @@ -1289,7 +1277,7 @@ { "cell": "절단기", "pum_table_id": "F0348", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", + "reason": "규격이 없어 같은 이름 여럿 중 고를 수 없음", "work_item_code": "FP-12-11-02" }, { @@ -1484,24 +1472,6 @@ "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", "work_item_code": "FP-13-06-01" }, - { - "cell": "굴착기 (무한궤도)", - "pum_table_id": "F0420", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-13-06-02" - }, - { - "cell": "굴착기 (무한궤도)", - "pum_table_id": "F0421", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-13-06-03" - }, - { - "cell": "굴착기 (무한궤도)", - "pum_table_id": "F0422", - "reason": "카탈로그에 없는 이름 (기계·자재 카탈로그 미확보 포함)", - "work_item_code": "FP-13-07-01" - }, { "cell": "굴 삭 기 (무한궤도)", "pum_table_id": "F0423",