Files
Aislo/M01_MasterData/M01_MasterData_Machine.py
eomsangdonandClaude Opus 5.5 7881ed9343 perf(826-2): 자재 한 줄 저장 15 초 → 1 초 · 기계 읽기 한 번에 풂
- Store_Make.save 새 키 찾기 = 3만 키 목록 훑기(제곱) → 묶음
- 기계 상세 = 판만 읽음(표 두 번 풀기 걷음) · 기계 폴더 새 사본 = 줄 글을 이어 한 번에 풂(표 열기 옛 한 파일과 비슷)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 20:18:00 +09:00

313 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M01 기계 화면(PLAN 821-2) — 양식 목록 · 시간당 경비(서버 값) · 쓰는 곳 · 비슷한 기계 찾기 ·
저장 전 기계 줄 손질(계수 서버 재계산 · 단위 · 출처 채움).
계수 = `master_machine_coef`(TS 짝 `common_util/common_util_machine_coef.ts`) ·
시간당 = 서버 기계경비식(`M02_Machine_Hourly.hourly` · 전국평균 · 기본 옵션) ·
쓰는 계산식 = `M02_Machine_Hourly.pick`(목록 › 꼴 · 822-2) · [새 기계] 저장 = 꼴에 맞는 식 목록에 원문번호 등록
(원문번호 빈 칸 = 「자체 EQ…」 받을 키) ·
M01 에서 만든 기계(출처 모두 「자체」)를 지우면 그 번호 기계가 더 없을 때 목록에서 뺌(`register`) ·
마스터 파일은 읽기만 — 쓰기는 `Store_Make.save`.
"""
from __future__ import annotations
import copy
import json
import re
from decimal import Decimal
from typing import Any
import master_machine_coef as mc # noqa: E402 (`store` 가 scripts 를 길에 둠)
from M01_MasterData import M01_MasterData_Store as store
FILE = "기계.json"
OWN_SOURCE = "자체"
_EQ = re.compile(r"EQ\d{6}")
_cache: dict[str, Any] = {"ver": None, "hourly": {}, "uses": None}
def _rows() -> list[dict]:
return store.read(FILE)[0].get("줄") or []
def _fresh() -> None:
"""마스터를 다시 읽었으면(저장 뒤) 셈 보관을 비움."""
ver = id(store.loaded()[1])
if _cache["ver"] != ver:
_cache.update(ver=ver, hourly={}, uses=None)
def _plain(v):
if isinstance(v, Decimal):
return int(v) if v == v.to_integral_value() else float(v)
return v
# ── 양식 목록 ────────────────────────────────────────────────────────
def lists() -> dict:
"""연료(유가 전국평균) · 직종(인력) · 전력 단가 · 시간환산 · 원문 · 구분 — 양식 고르기."""
files = store.loaded()[0]
oil = files.get("재료_유가전력.json", {}).get("줄") or []
fuels = [
{
"value": str(r["원문번호"]).split(":")[0],
"text": str(r["원문번호"]).split(":")[0],
"price": _plain(r.get("값")),
}
for r in oil
if r.get("구분") == "유류" and str(r.get("원문번호", "")).endswith(":전국평균")
]
power = next(
(_plain(r.get("값")) for r in oil if r.get("원문번호") == "일반용갑I저압:전력량_평균"), 0
)
jobs = [
{
"value": str(r["원문번호"]),
"text": f"{r['원문번호']} {r.get('이름') or ''}",
"price": _plain(r.get("값")),
}
for r in files.get("인력.json", {}).get("줄") or []
if isinstance(r.get("값"), (int, float, Decimal))
]
head = store.read(FILE)[0]
hour = Decimal(1) / Decimal(8)
for t in files.get("계수_건설품셈_08장_건설기계.json", {}).get("표") or []:
if t.get("키") == "CC000336":
got = next((x for x in t.get("줄") or [] if x.get("구분") == "시간환산"), None)
if got:
hour = Decimal(got["분자"]) / Decimal(got["분모"])
return {
"fuels": fuels,
"jobs": jobs,
"power": power,
"hour": float(hour),
"books": list(head.get("원문") or {}),
"groups": {k: (v or {}).get("원문") for k, v in (head.get("구분") or {}).items()},
}
# ── 시간당 경비 ──────────────────────────────────────────────────────
def hourly(keys: list[str]) -> dict[str, dict]:
"""키마다 {노무비 · 재료비 · 경비 · 계} 또는 {reason} — 마스터 판이 같으면 한 번만 셈."""
from M02_MasterTemplete import M02_Machine_Hourly as mh # M02 가 M01 을 부름 — 늦게
_fresh()
out = {}
for key in keys[:100]:
if key not in _cache["hourly"]:
got = mh.hourly(key)
_cache["hourly"][key] = (
{k: got[k] for k in ("노무비", "재료비", "경비", "계", "로직")}
if got.get("ok")
else {"reason": got.get("reason")}
)
out[key] = _cache["hourly"][key]
return out
def formula(row: dict) -> dict:
"""기계 줄(저장 전 고친 값도) → 쓰는 계산식 {키, 이름, 방법 목록|꼴|그 기계 식, 까닭}(822-2 상세 페이지)."""
from M02_MasterTemplete import M02_Machine_Hourly as mh # M02 가 M01 을 부름 — 늦게
code = str(row.get("원문번호") or "")
got = mh.pick({**row, "값": store._dec(row.get("값") or {})})
how = "목록" if mh.listed(code) else "그 기계 식" if got and mh._own(code) else "꼴"
base = {"방법": how, "까닭": mh.why({**row, "값": store._dec(row.get("값") or {})}, got)}
return base | ({"키": got["키"], "이름": got.get("이름")} if got else {})
def _family_files() -> dict[str, str]:
"""기계경비식 키 → 로직 파일 이름."""
return {
str(r.get("키")): name
for name, data in store.loaded()[0].items()
if data.get("그룹") == "로직" and "건설기계" in name
for r in data.get("줄") or []
}
def register(batch: list[dict]) -> None:
"""[새 기계] 저장 = 꼴 식 「기계」 목록에 그 원문번호 더함 · M01 에서 만든 기계(출처 모두 「자체」)를
지우면 같은 번호 기계가 더 없을 때 목록에서 뺌 — 로직 파일 고침을 같은 저장 묶음에 붙임(정본 저장 길)."""
from M02_MasterTemplete import M02_Machine_Hourly as mh # M02 가 M01 을 부름 — 늦게
from M02_MasterTemplete import M02_MyMachine as mym
part = next((p for p in batch if p.get("file") == FILE), None)
if part is None:
return
rows = _rows()
gone = {str(c.get("key")) for c in part.get("changes") or [] if c.get("op") == "delete"}
add: dict[str, list[str]] = {}
drop: set[str] = set()
for ch in part.get("changes") or []:
row = ch.get("row") if ch.get("op") == "add" else None
code = str((row or {}).get("원문번호") or "").strip()
if row and code and not mh.listed(code) and not mh._own(code):
key = mym.logic_key({**row, "값": store._dec(row.get("값") or {})})
if key and code not in add.get(key, []):
add.setdefault(key, []).append(code)
old = next((r for r in rows if r.get("키") == ch.get("key")), None)
if (
ch.get("op") == "delete"
and old
and set((old.get("출처") or {}).values()) == {OWN_SOURCE}
):
code = str(old.get("원문번호") or "")
if code and not any(
str(r.get("원문번호")) == code and r.get("키") not in gone for r in rows
):
drop.add(code)
if not add and not drop:
return
files = _family_files()
for name in dict.fromkeys(files.values()):
changes = []
for logic in store.read(name)[0].get("줄") or []:
ins = logic.get("입력") or []
pick = next((i for i in ins if i.get("이름") == "기계" and "고르기" in i), None)
if pick is None:
continue
codes = [c for c in pick["고르기"] if str(c) not in drop]
codes += [c for c in add.get(str(logic["키"]), []) if c not in codes]
if codes != pick["고르기"]:
new = copy.deepcopy(logic)
next(i for i in new["입력"] if i.get("이름") == "기계")["고르기"] = codes
changes.append({"op": "edit", "key": logic["키"], "row": new})
if changes:
batch.append({"file": name, "version": store.read(name)[1], "changes": changes})
# ── 쓰는 곳 ─────────────────────────────────────────────────────────
def _logic_index() -> tuple[dict[str, list[str]], dict[str, list[str]]]:
"""(원문번호 → 로직 「키 이름」 · 기계 키 → 로직) — 기계 고르기 · 식 안 EQ 키."""
_fresh()
if _cache["uses"] is None:
by_code: dict[str, list[str]] = {}
by_key: dict[str, list[str]] = {}
for data in store.loaded()[0].values():
if data.get("그룹") != "로직":
continue
for row in data.get("줄") or []:
label = f"로직 {row.get('키')} {row.get('이름') or ''}".strip()
for i in row.get("입력") or []:
if i.get("이름") == "기계":
for c in i.get("고르기") or []:
by_code.setdefault(str(c), []).append(label)
text = json.dumps(row, ensure_ascii=False, default=str)
for k in set(_EQ.findall(text)):
by_key.setdefault(k, []).append(label)
_cache["uses"] = (by_code, by_key)
return _cache["uses"]
def uses(key: str) -> list[str]:
"""그 기계를 쓰는 곳 — 로직(기계 고르기 · 식) · M02 단가산출(사본 · 줄에 키)."""
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
row = next((r for r in _rows() if r.get("키") == key), None)
if row is None:
raise store.StoreError(404, f"없는 기계 「{key}」")
by_code, by_key = _logic_index()
code = str(row.get("원문번호") or "")
out = list(dict.fromkeys([*(by_code.get(code, []) if code else []), *by_key.get(key, [])]))
mark = f'"{key}'.encode()
for where, path in layers.layer_files("sangeun"):
try:
raw = path.read_bytes()
except OSError:
continue
if mark in raw:
try:
name = json.loads(raw).get("이름") or path.stem
except ValueError:
name = path.stem
out.append(f"M02 단가산출 {where} {name}")
return out
# ── 비슷한 기계 찾기 · 한 줄 ─────────────────────────────────────────
def find(q: str, limit: int = 30) -> list[dict]:
words = [w for w in q.lower().split() if w]
out = []
for r in _rows():
hay = " ".join(str(r.get(k) or "") for k in ("이름", "규격", "별칭", "상세구분", "키"))
hay = hay.lower()
if all(w in hay for w in words):
out.append(_brief(r))
if len(out) >= limit:
break
return out
def _brief(r: dict) -> dict:
keep = ("키", "원문번호", "원문", "구분", "상세구분", "이름", "규격", "값", "별칭", "비고")
return {k: r[k] for k in keep if k in r}
def row(key: str) -> dict:
r = next((x for x in _rows() if x.get("키") == key), None)
if r is None:
raise store.StoreError(404, f"없는 기계 「{key}」")
return {"version": store.version(FILE), "row": r} # 판만(826-2)
# ── 저장 전 손질(`Store_Make.save` 가 부름) ──────────────────────────
def _units() -> dict[str, str]:
out: dict[str, str] = {}
for r in _rows():
for k, u in (r.get("단위") or {}).items():
out.setdefault(k, u)
return out
def _same_inputs(a: dict, b: dict) -> bool:
return all(
float(a[k]) == float(b[k]) if isinstance(b.get(k), (int, float, Decimal)) else False
for k in mc.INPUTS
)
def fix(part: dict) -> None:
"""기계 줄 고침 · 더함 — 계수는 서버가 짝으로 다시 셈(품셈 입력이 저장 값과 같으면
저장 값 그대로) · 단위 · 출처 묶음 = 값 칸과 같은 칸
(있던 칸 그대로 · 새 칸 = 마스터 단위 · 출처 「자체」)."""
if part.get("file") != FILE:
return
stored = {str(r.get("키")): r for r in _rows()}
units = _units()
issue = int(store.mk.load_book(store.FOLDER / store.mk.BOOK.name)["다음"].get("EQ", 1))
for ch in part.get("changes") or []:
row = ch.get("row")
if (
ch.get("op") == "add"
and isinstance(row, dict)
and not str(row.get("원문번호") or "").strip()
):
row["원문번호"] = (
f"{OWN_SOURCE} EQ{issue:06d}" # 822-2 식 목록에 넣을 번호(빈 칸 = 받을 키)
)
issue += 1
if (
ch.get("op") == "delete"
or not isinstance(row, dict)
or not isinstance(row.get("값"), dict)
):
continue
old = stored.get(str(ch.get("key") or "")) if ch.get("op") == "edit" else None
values = row["값"]
old_values = (old or {}).get("값") or {}
if all(isinstance(values.get(k), (int, float)) for k in mc.INPUTS):
if old and _same_inputs(values, old_values):
values.update({k: old_values[k] for k in mc.COEFS if k in old_values})
elif (got := mc.coef_of(values)) is not None:
values.update(got)
was_unit = (old or {}).get("단위") or {}
was_src = (old or {}).get("출처") or {}
unit, src = (
row.get("단위") or {},
row.get("출처") or {},
) # 보낸 칸이 있으면 그 글(822-2 빌더)
row["단위"] = {k: unit.get(k) or was_unit.get(k, units.get(k, "")) for k in values}
row["출처"] = {k: src.get(k) or was_src.get(k, OWN_SOURCE) for k in values}