feat(tester): 건설 PDF ↔ md 전수 커버리지 검사 + 겸용 연료 둘 갈라 읽기

브레인 B08·B14.

- **PDF ↔ md 커버리지**(`test_const_pdf_coverage.py`) — PDF 글자층을 좌표로 줄로 다시 묶어
  값 수를 뽑고 md 전문과 양방향으로 맞댐. 지금 **결손 749**(PDF 에 있는데 md 에 없음) ·
  **허구 547**(md 에만 — 대부분 붙은 줄 잔재 `0008900.90` 꼴)
  · 쪽별 셈을 목록에 두어 **늘면 빨강**(새 구멍) · `--report` 로 구멍 큰 쪽 스물을 뽑음
  · ⚠ 줄 짝은 안 맞춤(PDF 는 칸마다 줄바꿈) · 값처럼 생긴 수만(소수점·세 자리 이상, 연도 뺌) ·
    차례 줄·쪽번호 줄은 뺌
- **겸용 연료 둘**(`_build_mach.py`) — 3450-0642 현장가열 표층재생기 `73.7+휘발유54.5` ·
  7992-0001 모르타르 믹서 `1.87㎾ 휘발유1.3`
  · 둘째 연료는 `secondary_fuel_*`, 전력은 `electric_power_kw`(연료가 아니라 곁값)
  · 연료 줄 274 → 276 · 대조 시험도 같은 꼴을 읽게 넓혀 **기계 못 보는 자리 38 → 27**
  · 이름표에 새 열 셋 + 줄 수 갱신
- 전체 시험 2,442 통과

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 14:35:24 +09:00
co-authored by Claude Opus 5
parent 7786316ef5
commit 1660ca48ff
7 changed files with 1050 additions and 112 deletions
+41 -11
View File
@@ -254,24 +254,24 @@ def parse_fuel(
specs = split_values(row[spec_col], len(codes)) if spec_col is not None and spec_col < len(row) else None # fmt: skip
for n, code in enumerate(codes):
raw = values["fuel_rate_l_per_hour"][n]
# ⚠ 연료가 둘인 칸(`73.7+휘발유54.5`)은 한쪽만 적으면 틀림 — 안 싣고 셈만 남김
if _num(raw) is None or "+" in raw:
fuels, power = split_fuels(raw)
if not fuels:
# ⚠ 주연료가 수가 아닌 기종(전기 `㎾` · `-`)은 **연료 목록에 안 실음** — ℓ/hr 자료임.
skipped.append({"machine_code": code, "what": raw.strip()[:20]})
continue
record = {
"machine_code": code,
"machine_name": names.get(code[:4], ""),
# 표시 없는 것은 경유([주]① ㉰) · `휘발유0.7` · `중유487.2`
"fuel_type": (
"gasoline"
if re.search(r"휘발유|가솔린", raw)
else "heavy_oil"
if "중유" in raw
else "diesel"
),
"fuel_rate_l_per_hour": _num(raw),
"fuel_type": fuels[0][0],
"fuel_rate_l_per_hour": fuels[0][1],
}
# ⚠ **겸용 연료는 둘 다 싣는다** — 한쪽만 적으면 그만큼 경비가 빈다
# (`73.7+휘발유54.5` 현장가열표층재생기 · `1.87㎾ 휘발유1.3` 모르타르 믹서).
if len(fuels) > 1:
record["secondary_fuel_type"] = fuels[1][0]
record["secondary_fuel_rate_l_per_hour"] = fuels[1][1]
if power is not None:
record["electric_power_kw"] = power
for field in ("misc_material_percent_of_fuel", "operator_person_per_day"):
value = _num(values[field][n]) if field in values else None
if value is not None:
@@ -282,6 +282,36 @@ def parse_fuel(
return records, bad, source_tables, skipped
#: 겸용 연료 칸을 토막낸다 — `73.7+휘발유54.5` · `1.87㎾ 휘발유1.3` · `중유487.2`.
#: ⚠ 표시가 없으면 경유다([주]① ㉰).
_FUEL_PIECE_RE = re.compile(r"(휘발유|가솔린|중유|경유)?\s*([\d,]+(?:\.\d+)?)\s*(㎾|kW|kw)?")
def split_fuels(raw: str) -> tuple[list[tuple[str, float]], float | None]:
"""연료 칸 → (`[(연료갈래, ℓ/hr), …]`, 전력 ㎾).
한 칸에 연료가 둘인 기종이 있다 — 그 둘을 **다 싣는다**. 수가 없으면 빈 목록(전기·`-`).
"""
fuels: list[tuple[str, float]] = []
power: float | None = None
for mark, number, kw in _FUEL_PIECE_RE.findall(str(raw or "")):
value = _num(number)
if value is None:
continue
if kw: # 전력은 연료(ℓ/hr)가 아니라 곁값으로 적어 둔다
power = value
continue
kind = (
"gasoline"
if mark in ("휘발유", "가솔린")
else "heavy_oil"
if mark == "중유"
else "diesel"
)
fuels.append((kind, value))
return fuels, power
def extract(old_doc: dict, data: bytes) -> tuple[dict, dict]:
"""옛 벌 + 8장 md → (새 벌, 알림). 사람 판단 칸은 옛 벌에서 이어받음."""
lines = data.decode("utf-8").splitlines()
@@ -2,7 +2,7 @@
"schema_version": "1.0",
"dataset_id": "mach_base",
"effective_date": "2026-01-01",
"generated_at": "2026-09-18T13:19:25+09:00",
"generated_at": "2026-09-18T14:10:46+09:00",
"sources": [
{
"path": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제8장_건설기계.md",
@@ -14132,6 +14132,17 @@
"operator_person_per_day": 1,
"specification": "400"
},
{
"machine_code": "3450-0642",
"machine_name": "현장가열 표층재생기",
"fuel_type": "diesel",
"fuel_rate_l_per_hour": 73.7,
"secondary_fuel_type": "gasoline",
"secondary_fuel_rate_l_per_hour": 54.5,
"misc_material_percent_of_fuel": 20,
"operator_person_per_day": 7,
"specification": "479"
},
{
"machine_code": "3530-0015",
"machine_name": "스테이빌라이저(안정기)",
@@ -15085,6 +15096,15 @@
"operator_person_per_day": 1,
"specification": "11.19"
},
{
"machine_code": "7992-0001",
"machine_name": "모르타르 믹서",
"fuel_type": "gasoline",
"fuel_rate_l_per_hour": 1.3,
"electric_power_kw": 1.87,
"misc_material_percent_of_fuel": 2,
"specification": "0.3㎥"
},
{
"machine_code": "7993-0020",
"machine_name": "양수기",
@@ -19124,6 +19144,6 @@
"unparsed_price_rows": 0,
"unparsed_loss_rows": 10,
"unparsed_fuel_rows": 0,
"fuel_not_litre_rows": 30
"fuel_not_litre_rows": 28
}
}