- Store_Make.save 새 키 찾기 = 3만 키 목록 훑기(제곱) → 묶음 - 기계 상세 = 판만 읽음(표 두 번 풀기 걷음) · 기계 폴더 새 사본 = 줄 글을 이어 한 번에 풂(표 열기 옛 한 파일과 비슷) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
313 lines
13 KiB
Python
313 lines
13 KiB
Python
"""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}
|