feat(814-1 5): 시간당 경비 계산식 기계 목록 — 원문에 있는데 빠진 기계 더함

- 기계표 630 대 가운데 어느 계산식(GC000267 ~ 276)에도 없던 384 대를 자료대로 배정(빌더 build_기계목록_빠진기계_814_1)
  · 연료 · 조종원 = GC000268 92 · 조종원 직종 없음 = GC000275 18 · 조종원 없음 = GC000272 43 · 연료 없음 = GC000276 7 · 손료만 = GC000273 198
  · 못 넣음 50 줄(33 대) = 기계표 값 빔 · 범위(「13∼18」) — 지어 넣는 값 없음
- 로직 · 수식 문서 「기계」 목록 끝에 덧붙임(기본값 그대로) — 1,513 문서 기본 · 옵션 표본 18,797 금액 전후 같음 · 시간당 경비 595 대 셈 됨
- 조합(굴착기 + 브레이커 · 집게) = 우리 조합 식 GC000270 · 271 부름 글(`M02_Machine_Hourly.combo`)
- 802-2 대조 목록 다시 지음(옵션 표본 수만 늚)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-10 14:46:53 +09:00
co-authored by Claude Opus 5.5
parent 56a3e0538f
commit 8914b21bad
5 changed files with 543 additions and 10 deletions
@@ -0,0 +1,161 @@
"""시간당 경비 계산식(GC000267 ~ GC000276) 「기계」 고르기 · 선택 목록에 원문에 있는데 빠진 기계 더함(PLAN 814-1 · 요구 744 ~ 747).
기계표(EQ) 가운데 어느 계산식 목록에도 없는 기계를 자료(기계표 값)에 따라 식 하나에 배정 — 지어 넣는 값 없음:
연료(주연료 · 종류) · 조종원(인 · 직종) · 잡재료율 · 상각 · 정비 · 관리 계수 있음 = GC000268(기계경비)
연료 · 조종원(인) 있음 · 직종 없음 = GC000275(조종원직종 입력)
연료 있음 · 조종원 없음 = GC000272(조종원 없는 기종)
연료 없음 · 조종원(인 · 직종) 있음 = GC000276(연료 없는 기종)
연료 · 조종원(직종) 없음 · 가격 · 손료계수 있음 = GC000273(손료만 — 부착 장비 등)
그 식이 부르는 기계표 값이 비거나 수 하나가 아니면(「13∼18」) 안 넣고 「못 넣음」 목록에 적음.
8-1-7 손료 보정 식(GC000269 · GC000274)은 보정 기종 식에 그 기계 번호가 이미 있을 때만 같이 더함.
목록 끝에 덧붙임(기본값 그대로 = 기존 문서 금액 같음). 로직 파일(`로직_…_건설기계.json` 입력 고르기)과
수식 문서(`수식문서/수식_…_건설기계.json` 「기계」 선택 목록)를 같이 고침. 두 번 돌려도 같음.
쓰기: venv python resources/master_data/scripts/build_기계목록_빠진기계_814_1.py [--check]
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
DATA = HERE.parent
LOGIC = DATA / "로직_건설품셈_08장_건설기계.json"
DOC = DATA / "수식문서" / "수식_건설품셈_08장_건설기계.json"
FAMILY = [f"GC00027{i}" for i in range(6)] + ["GC000267", "GC000268", "GC000269"]
FAMILY = sorted(set(FAMILY) | {"GC000276"})
FIX = ("상각비계수", "정비비계수", "관리비계수")
NEEDS = {
"GC000268": ("가격", *FIX, "주연료종류", "주연료", "조종원직종", "조종원", "잡재료율"),
"GC000275": ("가격", *FIX, "주연료종류", "주연료", "조종원", "잡재료율"),
"GC000272": ("가격", *FIX, "주연료종류", "주연료", "잡재료율"),
"GC000276": ("가격", *FIX, "조종원직종", "조종원"),
"GC000273": ("가격", "손료계수"),
}
PAIR = {"GC000268": "GC000269", "GC000273": "GC000274"} # 8-1-7 보정 식
TEXT = ("주연료종류", "조종원직종") # 글 칸 — 그 밖 = 수 하나(범위 「13∼18」 · 빈 값 = 없음)
def _has(row: dict, field: str) -> bool:
v = (row.get("값") or {}).get(field)
if v in (None, ""):
return False
if field in TEXT:
return bool(str(v).strip())
try:
return float(str(v).replace(",", "")) != 0
except ValueError:
return False
def family_of(row: dict) -> tuple[str | None, list[str]]:
"""기계표 한 줄 → (식 키 | None, 빈 값 이름들) — 연료 · 조종원 유무로 식 가족을 고름."""
fuel, crew, job = _has(row, "주연료"), _has(row, "조종원"), _has(row, "조종원직종")
if fuel and crew:
key = "GC000268" if job else "GC000275"
elif fuel:
key = "GC000272"
elif crew and job:
key = "GC000276"
else:
key = "GC000273"
miss = [f for f in NEEDS[key] if not _has(row, f)]
return (None if miss else key), miss
def _rows() -> list[dict]:
data = json.loads((DATA / "기계.json").read_text("utf-8"))
return [
r for r in data.get("줄") or data.get("표") or [] if str(r.get("키", "")).startswith("EQ")
]
def _logic_lists(text: str) -> dict[str, list[str]]:
data = json.loads(text)
out = {}
for row in data["줄"]:
for i in row.get("입력") or []:
if row.get("키") in FAMILY and i.get("이름") == "기계":
out[row["키"]] = [str(c) for c in i.get("고르기") or []]
return out
def _span(text: str, key: str, start_pat: str) -> tuple[int, int]:
"""그 키 블록 안 첫 `start_pat` 뒤 배열 [ … ] 의 (여는 괄호, 닫는 괄호) 자리."""
at = text.index(f'"키": "{key}"')
m = re.compile(start_pat).search(text, at)
if not m:
raise SystemExit(f"{key} 기계 목록 자리를 못 찾음")
open_ = m.end() - 1
close = text.index("]", open_)
return open_, close
def _append(text: str, key: str, codes: list[str], start_pat: str) -> str:
if not codes:
return text
open_, close = _span(text, key, start_pat)
body = text[open_ + 1 : close]
if "\n" in body: # 여러 줄 꼴 — 마지막 줄 들여쓰기 그대로
last = body.rstrip().rsplit("\n", 1)[-1]
pad = last[: len(last) - len(last.lstrip())]
add = "".join(f',\n{pad}"{c}"' for c in codes)
cut = open_ + 1 + len(body.rstrip())
return text[:cut] + add + text[cut:]
add = "".join(f', "{c}"' for c in codes)
return text[:close] + add + text[close:]
LOGIC_PAT = r'"이름":\s*"기계",\s*"고르기":\s*\['
DOC_PAT = r'"열쇠":\s*"기계"[^\n]*?"목록":\s*\['
def plan() -> tuple[dict[str, list[str]], list[tuple[str, str, str, list[str]]]]:
logic = LOGIC.read_text("utf-8")
have = _logic_lists(logic)
taken = {c for v in have.values() for c in v}
fixes = {}
for r in json.loads(logic)["줄"]:
if r.get("키") in PAIR.values():
fixes[r["키"]] = " ".join(str(x.get("식") or "") for x in r.get("중간") or [])
add: dict[str, list[str]] = {}
skipped = []
for row in _rows():
code = str(row.get("원문번호") or "")
if not code or code in taken:
continue
key, miss = family_of(row)
if key is None:
skipped.append((code, str(row.get("이름")), str(row.get("규격") or ""), miss))
continue
if code in add.get(key, []):
continue
add.setdefault(key, []).append(code)
if PAIR.get(key) and f"'{code}'" in fixes.get(PAIR[key], ""):
add.setdefault(PAIR[key], []).append(code)
return add, skipped
def main(check: bool = False) -> None:
add, skipped = plan()
print("더함:", {k: len(v) for k, v in sorted(add.items())}, "· 못 넣음:", len(skipped))
for code, name, spec, miss in skipped:
print(f" 못 넣음 {code} {name} {spec} — 빈 값 {', '.join(miss)}")
if check or not add:
return
logic, doc = LOGIC.read_text("utf-8"), DOC.read_text("utf-8")
for key, codes in sorted(add.items()):
logic = _append(logic, key, codes, LOGIC_PAT)
doc = _append(doc, key, codes, DOC_PAT)
json.loads(logic), json.loads(doc) # 꼴 확인
LOGIC.write_text(logic, "utf-8", newline="")
DOC.write_text(doc, "utf-8", newline="")
if __name__ == "__main__":
main("--check" in sys.argv)