"""M02 단가산출 기계 창 초안(PLAN 827-1 · 요구 810 · 811) — 서버 단독. 초안 = SK 수식 문서 칸 `내기계초안` [{표지 「EQP초안-N」, 키?: 고칠 내 기계 키, 본뜬?: M01 · 내 기계 키, 줄: 몸}] · 줄 쌓기 기계 줄 입력 기계='개인 EQP초안-N' 이 가리킴 · 화면 캐시에만(디스크에 안 씀). 확인(`check`) = 몸 검사 + 셈 꼴 값 + 시간당(식 키) — 쓰기 없음(기계 창 [확인]). 미리 셈(`overlay`) = 그 요청 안만 `M02_MyMachine.DRAFT` 에 셈 꼴 줄을 얹음(사본 빈틈 = 초안 값으로 셈). 저장(`saving`) = 문서가 쓰는 초안만 만들기 · 고침 → 표지를 진짜 키로 → 같은 요청 안에서 SK 저장 · `TOUCH` = 사본 단가 줄을 새 값으로(`renew`) · 실패 = 만든 파일 지움 · 고친 줄 되돌림(번호표는 그대로). 사본 줄 보기(`copy_row`) = 그 SK 사본에 박힌 기계 줄 · 지금 줄 · 새 값 여부. 개인 층만(`mym._user` — 그 밖 404). """ from __future__ import annotations import json import re from collections.abc import Callable, Iterator from contextlib import contextmanager from contextvars import ContextVar from typing import Any from M02_MasterTemplete import M02_Item_Prices as prices from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MyMachine as mym FIELD = "내기계초안" TAG = re.compile(r"EQP초안-\d+") BODY = ("이름", "규격", "방식", "값", "시간당", "비고") #: 저장 한 요청 안 — 만든 · 고친 내 기계 키(사본 단가 줄을 새 값으로) TOUCH: ContextVar[frozenset] = ContextVar("초안고친키", default=frozenset()) def _bad(text: str, status: int = 400) -> store.StoreError: return store.StoreError(status, text) def drafts(doc: Any) -> list[dict]: got = doc.get(FIELD) if isinstance(doc, dict) else None if got is not None and not isinstance(got, list): raise _bad(f"「{FIELD}」 은 목록") return [d for d in got or [] if isinstance(d, dict)] def _have() -> dict[str, dict]: mym._user() return {r["키"]: r for r in mym.rows()} def _row(item: dict, have: dict[str, dict]) -> dict: """초안 하나 → 내 기계 줄(키 · 원문번호 = 표지) — 몸 검사 `mym.clean` · 고칠 키는 이 층에 있어야.""" tag = str(item.get("표지") or "") if not TAG.fullmatch(tag): raise _bad(f"초안 표지 「{tag}」") body = mym.clean(item.get("줄")) key = str(item.get("키") or "") if key: if key not in have: raise _bad(f"없는 내 기계 「{key}」", 404) keep = {k: have[key][k] for k in ("원문", "구분", "넣은날", "본뜬") if k in have[key]} else: base = str(item.get("본뜬") or "") keep = {"원문": "개인", "구분": "개인", **({"본뜬": base} if base else {})} return {**keep, "키": tag, "원문번호": f"{mym.NO}{tag}", "상세구분": body["이름"], **body} def check(item: Any) -> dict[str, Any]: """기계 창 [확인] — {줄, 셈값(셈 꼴 값 칸), 시간당(식 키), 연결(그 식 한 줄 셈 기록 = 화면 계산기 캐시)} · 쓰기 없음 · 틀리면 400 · `표지` = 화면이 정한 초안 표지(기록 요소 열쇠가 줄과 같게).""" item = item if isinstance(item, dict) else {} row = _row({**item, "표지": item.get("표지") or "EQP초안-0"}, _have()) eng = mym.engine(row) token = mym.DRAFT.set((eng,)) try: whole = mym.whole() hour = mym.hourly(eng, whole) rec = _record(whole, hour.get("로직"), row["원문번호"]) if hour.get("ok") else {} finally: mym.DRAFT.reset(token) out = {"줄": row, "셈값": prices.to_json(eng["값"]), "시간당": hour, "연결": rec} return json.loads(json.dumps(out, ensure_ascii=False, default=str)) def _record(whole: Any, logic: Any, no: str) -> dict: """그 식을 기계 하나로 부르는 한 줄 문서의 셈 기록(요소 · 표 · 문서) — 화면이 [확인] 직후 바로 셈.""" import master_doc_record as mr from M02_MasterTemplete import M02_Doc_Copy as dc var = {"기호": "P", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "단가산출", "키": logic, "입력": {"기계": f"'{no}'"}}} # fmt: skip line = {"이름": "기계", "단위": "hr", "비목": "묶음", "수량": "1", "단가": "P_기계"} one = {"꼴": "수식문서", "판": 1, "키": "_기계창", "이름": "기계", "결과단위": "원", "변수": [var], "식": [], "금액": [line]} # fmt: skip rec = mr.Rec(dc.ml.Links(whole, dc.closure(whole, [str(logic)]))) try: rec.prefetch(one) dc.mc.calc(one, rec) except dc._ERR: pass return dc.ml._norm(rec.out()) @contextmanager def overlay(doc: Any) -> Iterator[None]: """미리 셈 — 초안 셈 꼴 줄을 이 요청 안 `whole()` 위에 얹음(초안 없음 = 그대로).""" items = drafts(doc) if not items: yield return have = _have() token = mym.DRAFT.set(tuple(mym.engine(_row(i, have)) for i in items)) try: yield finally: mym.DRAFT.reset(token) def renew(copy: Any) -> Any: """사본 단가 줄 — 미리 셈 = 문서가 가리키는 초안 줄을 더함 · 저장 = 만든 · 고친 내 기계 줄을 새 값으로 (이 SK 는 「새 값」 아님).""" if not isinstance(copy, dict) or not isinstance(copy.get("단가"), list): return copy doc = copy.get("문서") if isinstance(copy.get("문서"), dict) else {} rest = {k: v for k, v in doc.items() if k != FIELD} used = set(TAG.findall(json.dumps(rest, ensure_ascii=False, default=str))) keys = TOUCH.get() | used if not keys: return copy from M02_MasterTemplete import M02_Sangeun_Copy as skc live = skc.whole_().index.get("EQ") or {} rows = [ prices.to_json(live[k]) if (k := str(r.get("키"))) in keys and k in live else r for r in copy["단가"] ] have = {str(r.get("키")) for r in rows} rows += [prices.to_json(live[k]) for k in sorted(used - have) if k in live] return {**copy, "단가": rows, "단가판": skc._fp(rows)} def _new(row: dict) -> str: """새 내 기계 파일(`mym.create` 와 같은 꼴 · 값은 초안 그대로 — 본뜬 값을 다시 섞지 않음).""" data = mym._read() key = f"{mym.PREFIX}{mym._user()}-{data['다음']:04d}" when = mym._now() head = {"키": key, "원문번호": f"{mym.NO}{key}", "원문": "개인", "구분": "개인"} body = {k: row[k] for k in ("상세구분", *BODY, "단위", "출처") if k in row} tail = {"넣은날": when, "고친날": when, **({"본뜬": row["본뜬"]} if row.get("본뜬") else {})} mym.atomic_write_json(mym._dir() / mym.NEXT, {"다음": data["다음"] + 1}) # 번호 먼저 mym._put({**head, **body, **tail}) return key def saving(body: dict, inner: Callable[[dict], Any]) -> Any: """[저장] 한 번 — 문서가 쓰는 초안 만들기 · 고침 → 표지 = 진짜 키 → `inner(새 몸)` · 실패 = 되돌림.""" doc = body.get("문서") if not isinstance(doc, dict) or FIELD not in doc: return inner(body) items = drafts(doc) rest = {k: v for k, v in doc.items() if k != FIELD} text = json.dumps(rest, ensure_ascii=False) used = set(TAG.findall(text)) if not used: return inner({**body, "문서": rest}) if lost := sorted(used - {str(i.get("표지")) for i in items}): raise _bad(f"초안 없는 기계 표지 {lost}") have = _have() todo = [(i, _row(i, have)) for i in items if str(i.get("표지")) in used] # 다 검사한 뒤 씀 made: list[str] = [] kept: list[dict] = [] names: dict[str, str] = {} try: for item, row in todo: if key := str(item.get("키") or ""): kept.append(have[key]) mym.update(key, {k: row[k] for k in BODY if k in row}) else: made.append(key := _new(row)) names[row["키"]] = key text = TAG.sub(lambda m: names[m.group(0)], text) token = TOUCH.set(frozenset(names.values())) try: return inner({**body, "문서": json.loads(text)}) finally: TOUCH.reset(token) except BaseException: for key in made: (mym._dir() / f"{key}.json").unlink(missing_ok=True) for old in kept: mym._put(old) raise def copy_row(doc: dict, copy: dict | None, no: str) -> dict[str, Any]: """기계 창 열 때 — {사본: 이 SK 사본에 박힌 줄 | None, 지금: 지금 줄 | None, 새값}(원문번호 · 키로 찾음).""" from M02_MasterTemplete import M02_Sangeun_Copy as skc no = no.strip().strip("'") pools = [(copy or {}).get("단가"), (doc.get(skc.KEY) or {}).get("단가")] have = next( (r for rows in pools for r in rows or [] if no in (r.get("원문번호"), r.get("키"))), None, ) whole = skc.whole_() live = (whole.numbers.get("EQ") or {}).get(no) or (whole.index.get("EQ") or {}).get(no) now = prices.to_json(live) if live is not None else None new = have is not None and now is not None and skc._fp(have) != skc._fp(now) return {"사본": have, "지금": now, "새값": new}