Files
Aislo/M02_MasterTemplete/M02_MyMachine_Draft.py
T
eomsangdonandClaude Opus 5.5 2149351b34 fix(827-1): 기계 창 [확인] 직후 금액 즉시 — 확인 길이 그 식 셈 기록을 줌
- 서버 확인 길(my-machines/draft) = 화면이 정한 표지로 그 식 한 줄 셈 기록(요소 · 표 · 식 문서) 돌려줌
- 화면은 기록을 계산기 캐시에 합침 → 서버 왕복 전 0 원 깜빡임 없음(미리 셈 값과 같음)
- 기계 창 목록 열 이름 「구분」 · 폭 조정
- 시험 test_827_1 ⑦ 셈 기록 확인 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 10:36:33 +09:00

209 lines
9.2 KiB
Python

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