feat(822-2): 기계 시간당 식 자료 — 새 식 넷 · 33 대 옮김 · 직종 13 대 · 전력 칸 · POWER TROWEL

- 08장 로직: GC001196 전력 기종 · GC001197 전력 + 조종원 · GC001198 손료계수 한 칸 연료 · GC001199 직접 값
- 손료만 식(GC000273) 목록에서 연료 · 조종원 · 전력 있는 기계 뺌 → 276 · 268 · 새 식 목록(보정 식 274 그대로)
- 기계표: 주연료 칸 ㎾ 글 10 대 → 전력(㎾) 숫자 칸(값원문 보존) · 원문 잡재료 「-」 = 0 · 조종원직종 13 대(요구 799)
- EQ000534 이름 POWER TLOWEL → POWER TROWEL(옛이름 보존)
- 가격 없는 기계(스프레어 둘 · TBM 4.5m) · 중유 믹싱플랜트 · 73.7+ 표층재생기는 목록 밖(꼴로 고름 → 까닭 글)
- 수식 문서 802-2 다시(1,518 저장 · 기본 다름 0) · 기존 M01 문서 1,514 금액 전후 같음
- M01 기계 저장 손질: 보낸 단위 · 출처 칸이 있으면 그 글

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 18:12:21 +09:00
co-authored by Claude Opus 5.5
parent 99338b3907
commit 58bebaf0c3
7 changed files with 849 additions and 155 deletions
@@ -0,0 +1,310 @@
"""기계 시간당 경비 식 완성(PLAN 822-2 · 요구 797 · 798 · 799) — 기계표 · 08장 로직 고침(정본 저장 길).
한 번만 돎(두 번째는 바뀐 것 없음):
기계.json
· 주연료 칸에 ㎾ 글이 든 전동 기계 → 숫자 칸 `전력`(㎾ · 전동기 부하율 규정 없음 → 그대로) · 주연료 비움
(원문 글은 `값원문` 그대로 · 비고에 한 줄) · 원문 잡재료 「-」 = 잡재료율 0
· 조종원직종 13 대 = 사용자 확정(요구 799) · 아스팔트 믹싱플랜트 잡재료 「-」 = 0
· EQ000534 이름 오자 「POWER TLOWEL」 → 「POWER TROWEL」(원문 8-4-8 표기 · `옛이름` 보존)
로직_건설품셈_08장_건설기계.json
· 새 식 넷 — 전력 기종 · 전력 + 조종원 · 손료계수 한 칸 연료 기종 · 시간당 값 직접
· 손료만 식(GC000273) 목록에서 옮긴 기계 뺌 → 맞는 식 목록에 더함(보정 식 GC000274 는 그대로)
저장 = `M01_MasterData_Store_Make.save`(검사 · 키 대장 · 줄 모양 그대로 · 개정확인).
수식 문서는 뒤에 `build_수식문서_802_2.py` 로 다시.
venv python resources/master_data/scripts/build_기계시간당_822_2.py [--check]
"""
from __future__ import annotations
import copy
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from M01_MasterData import M01_MasterData_Store as store # noqa: E402
from M01_MasterData import M01_MasterData_Store_Make as make # noqa: E402
MACHINE = "기계.json"
LOGIC = "로직_건설품셈_08장_건설기계.json"
JOB_SRC = "건설품셈 공통 8-1-3 5 · 건설노임 {}"
JOBS = { # 요구 799 — (직종, 까닭)
**{f"EQ00016{i}": ("1048", "건설기계관리법상 건설기계") for i in range(5, 10)},
**{
k: ("1048", "원문 8-1-3 5 표의 「쇄석기」")
for k in ("EQ000249", "EQ000250", "EQ000251", "EQ000252")
},
"EQ000347": ("1048", "원문 8-1-3 5 표의 「천공기」"),
"EQ000348": ("1048", "원문 8-1-3 5 표의 「천공기」"),
"EQ000309": ("1050", "두 법에 없는 기계"),
"EQ000433": ("1050", "두 법에 없는 기계"),
}
POWER = { # 주연료 칸 ㎾ 글 → 전력(원문 값 그대로)
"EQ000345": 330, "EQ000347": 135, "EQ000348": 239, "EQ000426": 15.1, "EQ000427": 15.1,
"EQ000529": 3.73, "EQ000530": 7.46, "EQ000531": 37, "EQ000533": 1.49, "EQ000532": 1.87,
} # fmt: skip
MISC_DASH = ("EQ000426", "EQ000427", "EQ000529", "EQ000530", "EQ000531", "EQ000533",
*(f"EQ0001{n}" for n in range(87, 92))) # fmt: skip
TYPO = ("EQ000534", "POWER TLOWEL", "POWER TROWEL")
P1, P2, N3, DIRECT = "전력", "전력조종원", "손료한칸연료", "직접"
MOVE = { # 식 → 더할 원문번호(손료만 식 GC000273 에서는 뺌)
"GC000276": ["2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "5105-0050",
"5105-0100", "5105-0150", "5105-0200", "5119-0625", "7205-0125"],
"GC000268": ["3108-0060", "3108-0080", "3108-0100", "3108-0120", "9010-0120",
"9010-0200"],
P1: ["7991-0050", "7202-0008", "7202-0010", "7991-0100", "7991-0500",
"7992-0001", "7993-0020"],
P2: ["5805-0002", "5805-0003"],
N3: ["7994-0050"],
} # fmt: skip
LOSS_ONLY = "GC000273"
#: 손료만 식에서 빼기만 — 주연료 「73.7+ 휘발유54.5」 를 풀지 않음(꼴로 고름 → 까닭 글)
DROP_ONLY = ["3450-0642"]
#: 목록 밖(꼴로 고름 → 값 모자람 까닭 글) — 가격 없는 기계를 목록에 두면 식 기본 · 옵션 대조가 갈림(802-2):
#: 아스팔트 스프레어 둘(가격 · 손료 없음 → 직종 입력 식 꼴) · TBM 4.5m(가격 없음 → 전력 식 꼴)
#: 3108-0040 = 주연료 중유 — 유가 표에 중유 없음(검사가 막음) → 목록 밖 · 꼴로 고름 → 까닭 글
LABEL = {P1: "전력", P2: "전력 조종원", N3: "손료계수 한 칸", DIRECT: "직접 값"}
_HEAD = {
"원문번호": "8",
"구분": "건설품셈 공통",
"상세구분": "08장 건설기계",
"결과단위": "원/시간",
}
_SRC = "건설품셈 공통 8-1-5 · 8-1-6"
_LOSS_ONE = {
"이름": "손료",
"식": "버림(EQ:{기계}.가격 * 1000 * EQ:{기계}.손료계수 / 10000000, 0)",
"출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4",
}
_HOUR = {
"이름": "시간환산",
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모",
"출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)",
}
_JOB = {"이름": "직종", "식": "EQ:{기계}.조종원직종", "출처": "건설품셈 공통 8-1-3 5"}
_CREW = {
"종류": "인력",
"요소": "LB:{직종}",
"이름": "조종원",
"단위": "인",
"수량": "EQ:{기계}.조종원 * 시간환산",
"비목": "노무비",
"출처": "건설품셈 공통 8-1-3 6",
"비고": "하루 노임 × 1/8(1일 8시간 · 건설품셈 공통 1-1-3 2)",
}
_ELEC_IN = {
"이름": "전력 계약종별",
"고르기": ["일반용(갑)Ⅰ 저압", "주택용 저압"],
"출처": "유가전력 한전 전기요금표 2025-01-01",
"비고": "품셈·계약예규에 계약종별 규정 없음(전력비는 실비 계상) — 기본 = 불러오기 기본값",
}
_ELEC = {
"종류": "재료",
"요소": {"이름": "전력량요금", "계약종별": "입력"},
"이름": "전력",
"단위": "㎾h",
"수량": "EQ:{기계}.전력",
"비목": "재료비",
"출처": "건설품셈 공통 8-1-6 3 가",
"비고": "기계 줄 전력(㎾) 그대로 — 전동기 부하율 규정 없음",
}
_FUEL = {"이름": "연료", "식": "EQ:{기계}.주연료종류", "출처": "건설품셈 공통 8-4 주①㉰"}
_FUEL_LINE = {
"종류": "재료",
"요소": "MO:{연료}:{지역}",
"이름": "주연료",
"단위": "ℓ",
"수량": "EQ:{기계}.주연료",
"비목": "재료비",
"출처": "건설품셈 공통 8-4-8",
}
_REGION = {"이름": "지역", "고르기": None, "출처": "건설품셈 공통 8-1-7 5"}
_MISC = {
"이름": "잡재료비",
"식": "재료비 * EQ:{기계}.잡재료율 / 100",
"비목": "재료비",
"출처": "건설품셈 공통 8-4 주②",
}
_LOSS_ADD = {"이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2"}
_CUT = "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음"
_S = {"기호": "S"}
def _vars(*names: str) -> dict:
sign = {"손료": "C", "시간환산": "Q"}
return {
n: {"기호": sign.get(n, "S"), "아래": n.replace("전력 계약종별", "전력계약종별")}
for n in names
}
def _machine_in(codes: list[str]) -> dict:
return {"이름": "기계", "고르기": codes}
def new_rows(regions: list) -> dict[str, dict]:
"""새 식 넷(키 없음 — 저장이 대장 다음 번호)."""
region = {**_REGION, "고르기": regions}
return {
P1: {
**_HEAD,
"이름": "기계 시간당 기계경비(전력 기종)",
"출처": f"{_SRC} · 8-1-6 3 가",
"부품": {"갈래": "기계경비식"},
"소유": "공용",
"비고": "원문 운전경비 표 주연료 칸에 ㎾ 가 찍힌 전동 기종 — 전력비 = 전력(㎾) × 전력량요금 · 잡재료 = 전력비의 %(8-4 주②)",
"입력": [_machine_in(MOVE[P1]), _ELEC_IN],
"중간": [_LOSS_ONE],
"호표": [_ELEC],
"덧줄": [_MISC, _LOSS_ADD],
"끝수": _CUT,
"변수표": _vars("기계", "전력 계약종별", "손료"),
},
P2: {
**_HEAD,
"이름": "기계 시간당 기계경비(전력 · 조종원 기종)",
"출처": f"{_SRC} · 8-1-6 3 가",
"부품": {"갈래": "기계경비식"},
"소유": "공용",
"비고": "원문 운전경비 표 주연료 칸에 ㎾ 가 찍히고 조종원이 있는 전동 기종(점보드릴) — 전력비 + 잡재료(전력비의 %) + 조종원",
"입력": [_machine_in(MOVE[P2]), _ELEC_IN],
"중간": [_LOSS_ONE, _JOB, _HOUR],
"호표": [_ELEC, _CREW],
"덧줄": [_MISC, _LOSS_ADD],
"끝수": _CUT,
"변수표": _vars("기계", "전력 계약종별", "손료", "직종", "시간환산"),
},
N3: {
**_HEAD,
"이름": "기계 시간당 기계경비(조종원 없는 기종 · 손료계수 한 칸)",
"출처": _SRC,
"부품": {"갈래": "기계경비식"},
"소유": "공용",
"비고": "원문 손료 표가 시간당 손료계수 하나만 준 기종(8-3-8 기타기계 등) 가운데 주연료가 있고 조종원이 없는 것",
"입력": [_machine_in(MOVE[N3]), region],
"중간": [_LOSS_ONE, _FUEL],
"호표": [_FUEL_LINE],
"덧줄": [_MISC, _LOSS_ADD],
"끝수": _CUT,
"변수표": _vars("기계", "지역", "손료", "연료"),
},
DIRECT: {
**_HEAD,
"이름": "기계 시간당 기계경비(직접 값)",
"출처": "자체",
"부품": {"갈래": "기계경비식"},
"소유": "공용",
"비고": "자체 — 구성 없는 기계 · 구성(손료 · 연료 · 조종원) 없이 시간당 노무비 · 재료비 · 경비를 바로 둔 기계(내 기계 「시간당 값 직접」) — 기계 줄 값 그대로",
"입력": [{"이름": "기계", "고르기": []}],
"중간": [],
"호표": [],
"덧줄": [
{
"이름": "시간당 노무비",
"식": "EQ:{기계}.노무비",
"비목": "노무비",
"출처": "자체",
},
{
"이름": "시간당 재료비",
"식": "EQ:{기계}.재료비",
"비목": "재료비",
"출처": "자체",
},
{"이름": "시간당 경비", "식": "EQ:{기계}.경비", "비목": "경비", "출처": "자체"},
], # fmt: skip
"끝수": None,
"변수표": _vars("기계"),
},
}
def _note(row: dict, text: str) -> None:
was = str(row.get("비고") or "")
if text not in was:
row["비고"] = f"{was} · {text}" if was else text
def machine_changes() -> list[dict]:
rows = {r["키"]: r for r in store.read(MACHINE)[0]["줄"]}
out = []
for key, row in rows.items():
new = copy.deepcopy(row)
vals, unit, src = new["값"], new.setdefault("단위", {}), new.setdefault("출처", {})
if key in POWER and vals.get("전력") is None:
text = str(vals.get("주연료"))
text += "㎾" if unit.get("주연료") == "㎾" else "" # 모르타르 믹서 = 단위 칸에 ㎾
got = re.search(r"([\d.]+)\s*㎾", text)
assert got and float(got.group(1)) == POWER[key], (key, text)
vals["주연료"], vals["전력"] = None, POWER[key]
unit["주연료"], unit["전력"] = "ℓ/hr", "kWh/hr"
src["전력"] = src.get("주연료")
_note(new, f"전력 = 원문 주연료 칸 「{text}」(㎾ · 전동기 부하율 규정 없음 → 그대로)")
if key == "EQ000532":
_note(new, "원문 「휘발유1.3」 갈래는 주연료2 칸 · 시간당 식은 전력")
if key in MISC_DASH and vals.get("잡재료율") is None:
vals["잡재료율"] = 0
_note(new, "잡재료율 0 = 원문 잡재료 「-」")
if key in JOBS and not vals.get("조종원직종"):
job, why = JOBS[key]
vals["조종원직종"] = job
src["조종원직종"] = JOB_SRC.format(job)
_note(new, f"직종 = 사용자 확정(요구 799) — {why}")
if key == TYPO[0] and new.get("이름") == TYPO[1]:
items = list(new.items())
at = [k for k, _ in items].index("이름") + 1
items.insert(at, ("옛이름", TYPO[1]))
new = dict(items)
new["이름"] = TYPO[2]
if new.get("상세구분") == TYPO[1]:
new["상세구분"] = TYPO[2]
if new != row:
out.append({"op": "edit", "key": key, "row": new})
return out
def logic_changes() -> list[dict]:
data = store.read(LOGIC)[0]
rows = {r["키"]: r for r in data["줄"]}
if any(r.get("이름") == "기계 시간당 기계경비(직접 값)" for r in rows.values()):
return []
moved = {c for codes in MOVE.values() for c in codes} | set(DROP_ONLY)
out = []
for key in (LOSS_ONLY, *[k for k in MOVE if k.startswith("GC")]):
new = copy.deepcopy(rows[key])
pick = next(i for i in new["입력"] if i.get("이름") == "기계")
if key == LOSS_ONLY:
pick["고르기"] = [c for c in pick["고르기"] if str(c) not in moved]
else:
pick["고르기"] += [c for c in MOVE[key] if c not in pick["고르기"]]
out.append({"op": "edit", "key": key, "row": new})
regions = next(i["고르기"] for i in rows["GC000268"]["입력"] if i["이름"] == "지역")
out += [{"op": "add", "row": row} for row in new_rows(regions).values()]
return out
def main(check: bool) -> None:
parts = []
for file, changes in ((MACHINE, machine_changes()), (LOGIC, logic_changes())):
print(file, len(changes), [c.get("key") or c["row"]["이름"] for c in changes])
if changes:
parts.append({"file": file, "version": store.read(file)[1], "changes": changes})
if check or not parts:
return
# 대장 열쇠 = 원문번호 — 넷 다 「8」 이면 한 키로 겹침 → 옛 기계경비식처럼 이름 붙은 열쇠(줄은 「8」)
labels = {r["이름"]: f"공통 8 기계 시간당 경비({LABEL[t]})" for t, r in new_rows([]).items()}
plain = store.ledger_label
store.ledger_label = lambda book, tid, no, row: (
labels.get(row.get("이름")) or plain(book, tid, no, row)
)
got = make.save(parts, confirmed=True)
print("저장", got["added"])
if __name__ == "__main__":
main("--check" in sys.argv)