"""M02 일위대가 실무자료 별칭 장부(PLAN 338-3 · 설계 3-5) — 층 `실무자료/맞춤.json` 을 키움. 줄 = {열쇠(품명|규격|단위 정규화): {원래 {품명, 규격, 단위}, 갈래 「짝」 · 「우리에 없음」 · 「안 씀」, 짝 [{로직|자재, 입력값, 배, 몫}], 누가, 때, 처음파일, 엑셀조항, 판}} · 직접 줄 호표 통째 = 열쇠 「호표|품명|규격|단위」. 옛 줄 {로직|자재|빼기, 입력값, 환산, 확정} = 읽을 때 새 꼴로 바꿔 읽음(빼기 = 「안 씀」 · 파일은 다음 쓰기 때 새 꼴). 읽는 차례 = 같은 층 → 위 층(개인 → 회사 → 마스터) · 아래 층이 이김 · 정확 열쇠가 먼저 · 없으면 품명|단위 느슨 열쇠(`규격다름`). 쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) · 짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]). """ from __future__ import annotations from datetime import datetime from decimal import Decimal from pathlib import Path from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store mm = m01.cm.mf.mp.mm KINDS = ("짝", "우리에 없음", "안 씀") HOPYO = "호표|" # ── 열쇠 ───────────────────────────────────────────────────────────── def key(line: dict[str, Any]) -> str: """줄 열쇠 = 품명 | 규격 | 단위(정규화 · `M02_Hopyo_Match.line_key` 와 같음).""" return "|".join( ( mm.plain(line.get("이름")), mm.plain_spec(line.get("규격")), mm.plain_unit(line.get("단위")), ) ) def hopyo_key(hopyo: dict[str, Any]) -> str: """직접 줄 호표 통째 열쇠 — 「호표|품명|규격|단위」.""" return HOPYO + key(hopyo) def _loose(full: str) -> str: """느슨 열쇠 = 품명 | 단위(규격 뺌).""" head, _, rest = full.rpartition("|") return f"{head.rsplit('|', 1)[0]}|{rest}" # ── 옛 꼴 → 새 꼴 ───────────────────────────────────────────────────── def _plain_of(full: str) -> dict[str, str]: name, spec, unit = full.removeprefix(HOPYO).split("|") return {"품명": name, "규격": spec, "단위": unit} def upgrade(full: str, raw: Any) -> dict[str, Any] | None: """장부 한 줄을 새 꼴로 — 옛 기억 줄 · 새 꼴 · 모르는 꼴(None).""" if not isinstance(raw, dict): return None if raw.get("갈래") in KINDS: return {"원래": _plain_of(full), "짝": [], **raw} head = {"원래": _plain_of(full), "때": raw.get("확정"), "누가": None, "처음파일": None} if raw.get("빼기"): return {**head, "갈래": "안 씀", "짝": []} pair = {"로직": raw["로직"]} if raw.get("로직") else {"자재": raw.get("자재")} if not any(pair.values()): return None pair.update(입력값=raw.get("입력값") or {}, 배=raw.get("환산"), 몫=None) return {**head, "갈래": "짝", "짝": [pair]} def read(folder: str | Path) -> dict[str, dict[str, Any]]: """한 층 장부 `{열쇠: 새 꼴 줄}`(옛 꼴은 바꿔 읽음 · 모르는 줄 뺌).""" out = {} for full, raw in hopyo_store.memory(folder).items(): if str(full).removeprefix(HOPYO).count("|") == 2 and (got := upgrade(str(full), raw)): out[str(full)] = got return out # ── 층 ─────────────────────────────────────────────────────────────── def chain(folder: str | Path) -> list[Path]: """장부를 볼 층 폴더 — 같은 층 → 위 층(개인 → 회사 → 마스터).""" layer, owner = refs.where(folder) out = [Path(folder)] if layer == "personal": out.append(refs.folder_of("company", str(owner).split("/")[0])) if layer in ("personal", "company"): out.append(Path(store.FOLDER)) return out def books(folder: str | Path) -> list[tuple[str, str | None, dict[str, dict[str, Any]]]]: """층마다 `(층, 주인, 장부)` — 같은 층부터(줄 여럿을 찾을 때 한 번 읽어 넘김).""" return [(*refs.where(one), read(one)) for one in chain(folder)] def recall( line: dict[str, Any], folder: str | Path, got: list | None = None, whole: bool = False ) -> dict[str, Any] | None: """줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름}` · 없으면 None. 정확 열쇠(아래 층 먼저) → 품명|단위 느슨(규격다름 True · 체크 안 함).""" full = hopyo_key(line) if whole else key(line) got = books(folder) if got is None else got for layer, owner, book in got: if full in book: return {**book[full], "층": layer, "주인": owner, "규격다름": False} loose = _loose(full) for layer, owner, book in got: for one, row in book.items(): if one.startswith(HOPYO) == whole and _loose(one) == loose: return {**row, "층": layer, "주인": owner, "규격다름": True} return None def rows(folder: str | Path, writable: bool) -> list[dict[str, Any]]: """장부 화면 — 이 층 + 위 층 줄 `{열쇠, …줄, 층, 주인, 고침}`(고침 = 이 층 · 쓰기 권한).""" out = [] for at, (layer, owner, book) in enumerate(books(folder)): for full, row in sorted(book.items()): out.append( {"열쇠": full, **row, "층": layer, "주인": owner, "고침": writable and not at} ) return out # ── 검사 · 쓰기 ─────────────────────────────────────────────────────── def _bad(text: str) -> store.StoreError: return store.StoreError(400, text) def _number(raw: Any) -> Decimal | None: if isinstance(raw, bool): return None try: got = Decimal(str(raw).replace(",", "").strip()) except ArithmeticError: return None return got if got.is_finite() else None def _json(value: Decimal) -> int | float: value = value.normalize() return int(value) if value == value.to_integral_value() else float(value) def _known() -> tuple[set[str], set[str]]: """(돈 로직 키, M01 자재품목 키) — 맞춤 목록 한 벌(`M02_Hopyo_Match.targets`).""" from M02_MasterTemplete import M02_Hopyo_Match as match # 맞춤이 장부를 부름 — 늦게 읽음 got = match.targets() return {x["로직"] for x in got["로직"]}, {x["자재"] for x in got["자재"]} def input_names(code: str) -> set[str]: got = labor_inputs.logic_inputs([code])["로직"] return {str(x.get("이름")) for x in (got[0]["입력"] if got else [])} def check_pair(raw: Any, where: str, known: tuple[set[str], set[str]]) -> dict[str, Any]: """짝 하나 재검사 → `{로직|자재, 입력값, 배, 몫}` — 틀리면 400.""" if not isinstance(raw, dict): raise _bad(f"짝 꼴이 틀림 「{where}」") extra = set(raw) - {"로직", "자재", "입력값", "배", "몫", "이름", "단위"} if extra: raise _bad(f"모르는 칸 {sorted(extra)} 「{where}」") code, mat = str(raw.get("로직") or ""), str(raw.get("자재") or "") if bool(code) == bool(mat): raise _bad(f"돈 로직 · 우리 자재 하나만 「{where}」") if (code and code not in known[0]) or (mat and mat not in known[1]): raise _bad(f"없는 {'로직' if code else '자재'} 「{code or mat}」 「{where}」") values = raw.get("입력값") or {} if not isinstance(values, dict) or (mat and values): raise _bad(f"입력값이 틀림 「{where}」") unknown = set(values) - input_names(code) if code else set() if unknown: raise _bad(f"모르는 입력 {sorted(unknown)} 「{where}」") times = _number(raw.get("배", 1)) if times is None or times <= 0: raise _bad(f"배는 0 넘는 수 「{where}」") share = raw.get("몫") part = None if share is None else _number(share) if share is not None and (part is None or not 0 < part <= 1): raise _bad(f"몫은 0 넘고 1 이하 「{where}」") head = {"로직": code} if code else {"자재": mat} return { **head, "입력값": values, "배": _json(times), "몫": None if part is None else _json(part), } def check(full: str, raw: Any, known: tuple[set[str], set[str]] | None = None) -> dict[str, Any]: """장부 한 줄 재검사(장부 화면 [저장]) → 쓸 꼴(누가 · 때 빼고).""" if str(full).removeprefix(HOPYO).count("|") != 2 or not isinstance(raw, dict): raise _bad(f"장부 꼴이 틀림 「{full}」") kind = raw.get("갈래") if kind not in KINDS: raise _bad(f"갈래는 {' · '.join(KINDS)} 「{full}」") pairs = raw.get("짝") or [] if kind != "짝" and pairs: raise _bad(f"「{kind}」 줄은 짝 없음 「{full}」") if kind == "짝" and (not isinstance(pairs, list) or not pairs or len(pairs) > 20): raise _bad(f"짝은 하나 넘고 스물 이하 「{full}」") known = known or _known() origin = raw.get("원래") if isinstance(raw.get("원래"), dict) else _plain_of(full) return { "원래": {k: str(origin.get(k) or "") for k in ("품명", "규격", "단위")}, "갈래": kind, "짝": [check_pair(p, full, known) for p in pairs], "엑셀조항": raw.get("엑셀조항") if isinstance(raw.get("엑셀조항"), list) else [], } def _version() -> str | None: """마스터 판 열쇠(가 창 `M02_Hopyo_Ours.version` — 아직 없으면 None).""" try: from M02_MasterTemplete import M02_Hopyo_Ours as ours except ImportError: return None return ours.version() if hasattr(ours, "version") else None def write( folder: str | Path, entries: dict[str, dict[str, Any] | None], who: Any, file: str | None = None, ) -> int: """검사 끝난 줄을 층 장부에 씀(None = 지움) — 같은 열쇠는 덮음 · 처음파일은 있던 값 둠 · 옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수.""" if not entries: return 0 have = read(folder) stamp = datetime.now().isoformat(timespec="seconds") version = _version() out: dict[str, dict[str, Any] | None] = dict(have) # 옛 꼴도 새 꼴로 다시 씀 for full, row in entries.items(): if row is None: out[full] = None continue first = (have.get(full) or {}).get("처음파일") or file out[full] = {**row, "누가": who, "때": stamp, "처음파일": first, "판": version} hopyo_store.remember(folder, out) return len(entries) def put(folder: str | Path, entries: Any, who: Any) -> dict[str, Any]: """장부 화면 [저장] — `{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀, 줄}`.""" if not isinstance(entries, dict) or not entries or len(entries) > 2000: raise _bad("저장할 줄이 없음") known = _known() clean = {str(k): None if v is None else check(str(k), v, known) for k, v in entries.items()} return {"씀": write(folder, clean, who), "줄": rows(folder, True)}