diff --git a/resources/tester/fixtures/base_prices_source_known.json b/resources/tester/fixtures/base_prices_source_known.json index 94a0eb6a..407f5615 100644 --- a/resources/tester/fixtures/base_prices_source_known.json +++ b/resources/tester/fixtures/base_prices_source_known.json @@ -212,9 +212,9 @@ "why": "자료 이름이 빔 — 원문 손료표 머리 `(코드) 이름` 엔 이름 있음" }, "셈 손료 코드 수": { - "source": 586, + "source": 608, "data": 387, - "why": "원문 586 줄 중 자료 387 — 뭉친 줄·해상장비·`-0003` 줄을 자료가 안 옮김(`… 자료에 없음` 참조)" + "why": "원문 손료 코드 608(코덱스 복원으로 586 에서 늘어남) 중 자료 387 — 자료에 없는 기종 묶음(`… 자료에 없음` 참조)" }, "셈 연료 코드 수": { "source": 197, @@ -246,6 +246,18 @@ "data": null, "why": "축약 손료표(내용시간·계만) — 자료가 내용시간을 안 옮김" }, + "손료 0230 자료에 없음": { + "source": [ + "0230-0002", + "0230-0004", + "0230-0006", + "0230-0007", + "0230-0008", + "0230-0010" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 0240-0007 machine_name": { "source": "유압식 진동콤팩터(굴착기 부착용)", "data": "대형 브레이커시 간 당(10-7)연간분류규격내용연간표준상각정비관리상각비정비비관리비번호(㎥)시간가동시간비율비율계비율계수계수계수0230-00020.23,0008900.90.850.13,0002,8337686,60100040.43,0008900.90.850.13,0002,8337686,60100060.63,0008900.90.850.13,0002,8337686,60100070.73,0008900.90.850.13,0002,8337686,60100080.83,0008900.90.850.13,0002,8337686,60100101.03,0008900.90.850.13,0002,8337686,601(0240) 유압식 진동콤팩터(굴착기 부착용)", @@ -290,6 +302,17 @@ "data": null, "why": "원문 줄 코드가 `-0003` 꼴 — 자료가 못 읽고 빠뜨림" }, + "손료 3601 자료에 없음": { + "source": [ + "3601-0102", + "3601-0202", + "3601-0204", + "3601-0302", + "3601-0402" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 3611-0142 machine_name": { "source": "콘크리트 피니셔(중앙분리대용)", "data": "콘크리트 피니셔(포장용)", @@ -500,11 +523,25 @@ "data": null, "why": "원문 한 칸에 여러 코드가 뭉친 줄 — 자료가 안 옮겨 손료가 없음" }, + "손료 6532 자료에 없음": { + "source": [ + "6532-0220" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 6540-0131 machine_name": { "source": "워터젯트", "data": "진동파일 해머(유압식)시 간 당(10-7)연간분류규격내용연간표준상각정비관리상각비정비비관리비번호(㎾)시간가동시간비율비율계비율계수계수계수6532-02201627,0008900.90.50.11,2867146822,682(6540) 워터젯트", "why": "자료 이름 칸에 원문 뭉개진 손료 글이 통째로 들어감 — 자료 이름 틀림" }, + "손료 6801 자료에 없음": { + "source": [ + "6801-0010" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 6802-0040 machine_name": { "source": "파일천공전용장비", "data": "고압분사전용장비", @@ -535,6 +572,20 @@ "data": "고압분사전용장비", "why": "앞 기종 6801 이름이 붙음 — 원문 뭉개진 글(L2927) 경계에서 밀림" }, + "손료 7101 자료에 없음": { + "source": [ + "7101-0450" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, + "손료 7120 자료에 없음": { + "source": [ + "7120-0746" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 7202 자료에 없음": { "source": [ "7202-0008", @@ -593,6 +644,13 @@ "data": null, "why": "원문 한 칸에 여러 코드가 뭉친 줄 — 자료가 안 옮겨 손료가 없음" }, + "손료 7830 자료에 없음": { + "source": [ + "7830-0081" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 7930-0001 machine_name": { "source": "모터", "data": "우레탄폼 분사용기구", @@ -739,6 +797,18 @@ "data": null, "why": "해상장비 손료표(형식·출력 여러 칸) — 자료가 안 옮김" }, + "손료 9060 자료에 없음": { + "source": [ + "9060-0060", + "9060-0100", + "9060-0200", + "9060-0300", + "9060-0500", + "9060-0600" + ], + "data": null, + "why": "코덱스 복원(0a3908b5·42073a6d)으로 원문에서 새로 읽히게 된 손료 줄 — 자료가 아직 안 실음" + }, "손료 9070 자료에 없음": { "source": [ "9070-0015", @@ -860,69 +930,22 @@ } }, "machine_blind": { - "손료 뭉개진 글 L1947": { + "손료 계수 칸 없는 표 5220": { "what": [ - "0230" + "분류 번호", + "규 격", + "내용 시간", + "연간 표준 가동 시간" ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" + "why": "복원 뒤에도 원문 표에 계수 칸이 없음 — 머리 「시 간 당(10-7)」 은 표 위 글줄로만 · 값은 원문에 없음" }, - "손료 뭉개진 글 L2384": { + "손료 계수 칸 없는 표 7995": { "what": [ - "3601" + "분류 번호", + "규 격", + "내용 시간" ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L2683": { - "what": [ - "5220" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L2894": { - "what": [ - "6532" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L2927": { - "what": [ - "6801" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L2956": { - "what": [ - "7101" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L2988": { - "what": [ - "7120" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L3209": { - "what": [ - "7830" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L3297": { - "what": [ - "7995" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L3346": { - "what": [], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" - }, - "손료 뭉개진 글 L3367": { - "what": [ - "9060" - ], - "why": "원문 손료표가 표가 아니라 한 줄 글로 뭉개짐 — 코드·값을 못 가름, 이 기종 손료는 안 봄(자료에도 없음)" + "why": "복원 뒤에도 원문 표에 계수 칸이 없음 — 머리 「시 간 당(10-7)」 은 표 위 글줄로만 · 값은 원문에 없음" }, "손료보정 수 자리 가름": { "what": "뭉개진 글의 이어 붙은 수(2510)를 통째로 맞댐", @@ -944,6 +967,10 @@ "what": "원문과 안 맞댐", "why": "계산 칸(계 × 10⁻⁷) — 원문 `계`(source_coefficient_1e_minus_7)만 맞댐" }, + "안 잰 칸 mach_loss_coef.note": { + "what": "원문과 안 맞댐", + "why": "데스크탑 서브가 붙인 설명 글 — 값 아님" + }, "안 잰 칸 mach_loss_coef.specification": { "what": "원문과 안 맞댐", "why": "규격 칸이 한 칸에 여러 값·여러 열로 뭉쳐 가르기 어려움 — 안 맞댐" @@ -1040,28 +1067,28 @@ "what": "원문 표 토막 수가 안 맞음", "why": "원문 운전경비 표 토막 수가 코드 수와 안 맞음(`동력 15.1㎾`·`73.7+ 휘발유54.5` 등) — 이 줄 자료 값은 못 봄" }, - "연료 표 L3420": { + "연료 표 3108-0040": { "codes": [ "3108-0040", "3901-0300" ], "why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음" }, - "연료 표 L3444": { + "연료 표 5401-0015": { "codes": [ "5401-0015", "5805-0003" ], "why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음" }, - "연료 표 L3456": { + "연료 표 7101-0450": { "codes": [ "7101-0450", "7811-0120" ], "why": "원문 운전경비 표 토막 수가 안 맞음 — 이 표 줄은 못 읽음" }, - "연료 표 L3462": { + "연료 표 7812-0005": { "codes": [ "7812-0005", "7994-0050" @@ -1157,6 +1184,14 @@ "what": "코드(`B09_Estimation_Statutory.STATUTORY_ITEMS`)가 직접공사비로 계산하고 있는 것과 맞춘 표기.", "why": "설명 글" }, + "안 잰 칸 rate_overhead.basis": { + "what": "조달청 「2026년 토목·조경·산업환경설비공사 원가계산 간접공사비(제비율) 적용기준」(2026-04-13). ⚠ 구간 라벨의 `billion` 은 **십억 원** — `lt_5_billion` 은 50억 미만이다. 종합(토목·조경·산업설비)은 5억미만~30억~50억 8 · 50~100억·100~300억 6.5 · 300~1000억 5 · 1000억이상 4.5, 전문 등은 5억미만 8 · 5~30억 6.5 · 30~50억·50~100억 5 · 100억이상 4.5.", + "why": "데스크탑 서브가 붙인 설명 글 — 값 아님" + }, + "안 잰 칸 rate_overhead.specialty_note": { + "what": "⚠ 2026-09-18 **300억 이상 두 구간이 통째로 빠져 있었음** — 마지막이 「100억-300억 미만」으로 닫혀 300억 넘는 전문공사가 어느 구간에도 안 걸렸음. 원본 XLSX(BL35:BR46) 대로 300~1000억 · 1000억 이상을 더함(둘 다 4.5%). 값은 원문 그대로.", + "why": "데스크탑 서브가 붙인 설명 글 — 값 아님" + }, "안 잰 칸 rate_performance_guarantee_fee.typical_forest_road_applicability": { "what": "not_applicable", "why": "임도 적용 판단 — 원문 칸 아님" diff --git a/resources/tester/test_base_prices_source_match.py b/resources/tester/test_base_prices_source_match.py index 99133a4f..cd50179d 100644 --- a/resources/tester/test_base_prices_source_match.py +++ b/resources/tester/test_base_prices_source_match.py @@ -221,7 +221,7 @@ def read_machine(path: Path) -> dict: codes, prefix = _codes(r[1], prefix) prices = r[2].split() if codes is None or len(codes) != len(prices): - out["skipped"][f"가격 줄 L{t['line'] + 2 + n}"] = {"what": r[1][:40]} + out["skipped"][f"가격 줄 {_t(r[1])[:24]}"] = {"what": r[1][:40]} continue out["price"].update( (c, {"price_thousand_krw": _num(p)}) for c, p in zip(codes, prices) @@ -235,8 +235,11 @@ def read_machine(path: Path) -> dict: sub = next((r for r in t["rows"] if not r[0]), []) if "상각비 계수" in sub: cols.update(zip(LOSS_COEF, range(sub.index("상각비 계수"), 99))) - else: + elif "시간당(10-7)" in head: cols[LOSS_COEF[3]] = head.index("시간당(10-7)") + else: # 원문 표에 계수 칸이 없음(5220·7995 — 머리 「시 간 당(10-7)」 은 표 위 글줄로만 남음) + out["skipped"][f"손료 계수 칸 없는 표 {t['rows'][0][0][:4] if t['rows'] else '?'}"] = {"what": t["headers"]} # fmt: skip + continue for n, r in enumerate(t["rows"]): if not r[0]: continue @@ -246,7 +249,7 @@ def read_machine(path: Path) -> dict: if not codes or any(len(v) != len(codes) for v in values.values()): raise ValueError(r[0]) except ValueError: - out["skipped"][f"손료 줄 L{t['line'] + 2 + n}"] = {"what": r[0][:40]} + out["skipped"][f"손료 줄 {_t(r[0])[:24]}"] = {"what": r[0][:40]} continue out["loss"].update( (c, {f: v[k] for f, v in values.items()}) for k, c in enumerate(codes) @@ -254,12 +257,13 @@ def read_machine(path: Path) -> dict: for k in range(at["8-3 기계손료"], at["8-4 운전경비"]): if not lines[k - 1].lstrip().startswith("|") and "시 간 당(10-7)" in lines[k - 1]: heads = re.findall(r"\((\d{4})\) ?(?:[^|()]{0,30}\([^)]*\)){0,2}[^|()]{0,30}?시 간 당\(10-7\)", lines[k - 1]) # fmt: skip - out["skipped"][f"손료 뭉개진 글 L{k}"] = {"what": heads} + if heads: # 표 위 머리 글줄(`시 간 당(10-7)` 한 줄)은 표로 읽힘 — 뭉개진 글만 + out["skipped"][f"손료 뭉개진 글 {'·'.join(heads)}"] = {"what": heads} prefix = None for t in tables: if not at["8-4 운전경비"] < t["line"] < at["8-5 기계가격"]: continue - out["fuel_tables"][t["line"]] = {"headers": t["headers"], "rows": t["rows"]} + out["fuel_tables"][len(out["fuel_tables"])] = {"headers": t["headers"], "rows": t["rows"]} if t["line"] > at["8-4-9 [90]"] or t["headers"][0] != "분류번호": continue for r in t["rows"]: @@ -270,7 +274,9 @@ def read_machine(path: Path) -> dict: raise ValueError(r[0]) parsed = [(*_fuel(f), _num(m), _num(o)) for f, m, o in zip(*cols)] except ValueError: - out["skipped"][f"연료 표 L{t['line']}"] = {"codes": codes and [codes[0], codes[-1]]} + out["skipped"][f"연료 표 {codes[0] if codes else _t(r[0])[:24]}"] = { + "codes": codes and [codes[0], codes[-1]] + } out["fuel_unread"] = out.get("fuel_unread", set()) | set(codes or ()) continue out["fuel"].update((c, dict(zip(FUEL_FIELDS, p))) for c, p in zip(codes, parsed) if p[1] is not None) # fmt: skip @@ -315,10 +321,15 @@ def check_machine(src: dict, bad: dict, blind: dict) -> dict: _unchecked(blind, var, records, {"machine_code", "machine_name", *fields}) blind.update(source["skipped"]) tables = v["mach_fuel_rate"]["source_tables"] - for st in tables: - t = source["fuel_tables"].get(st["line"]) + for n, st in enumerate( + tables + ): # ⚠ 줄 번호로 안 맞댐 — md 가 고쳐지면 통째로 밀림. 8-4 안 표 차례로 + t = source["fuel_tables"].get(n) if t != {"headers": st["headers"], "rows": st["rows"]}: - bad[f"운전경비 원표 L{st['line']}"] = {"source": t, "data": _without_why(st)} + bad[f"운전경비 원표 {n + 1}번째"] = { + "source": t, + "data": {k: st[k] for k in ("headers", "rows")}, + } counts["운전경비 원표 수"] = (len(source["fuel_tables"]), len(tables)) _unchecked(blind, "mach_fuel_rate.source_tables", tables, {"line", "headers", "rows"})