"""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}