test(z01): 기초단가 원천 시험 — 손료표 복원 뒤 멈추던 것 · 줄 번호 열쇠를 표 차례·코드로

코덱스 2단 머리 복원(42073a6d)으로 손료표 13개는 풀림. 남은 둘에서 시험이 멈추고(15건 오류) 있었음.

- 5220 소형브레이커 · 7995 배관파이프 — 머리 「시 간 당(10-7)」 은 표 위 글줄로 돌아왔으나 표에 계수 칸이
  없음(원문에 값 없음) → 멈추지 않고 「손료 계수 칸 없는 표」 로 못 보는 자리에 남김
- ⚠ 목록 열쇠·원표 맞대기가 줄 번호에 기대고 있었음(`손료 줄 L2683` · 운전경비 원표를 `line` 으로 찾음)
  → md 가 고쳐질 때마다 통째로 밀림. 코드·8-4 안 표 차례로 바꿈
- 복원으로 원문에서 새로 읽히는 손료 줄 → 자료에 없는 기종 8 묶음(0230·3601·6532·6801·7101·7120·7830·9060)
  · 원문 손료 코드 586 → 608 · 뭉개진 손료 글 11 이 모두 표로 돌아와 못 봄 목록에서 빠짐

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-18 01:50:31 +09:00
co-authored by Claude Opus 5
parent f20cd58bab
commit 51bd93ebf3
2 changed files with 119 additions and 73 deletions
@@ -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": "임도 적용 판단 — 원문 칸 아님"
@@ -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"})