Files
Aislo/M02_MasterTemplete/M02_Doc_Sk_Path.py
T
eomsangdonandClaude Opus 5.5 98f37c7c25 feat(827-1): 단가산출 기계 창 — 조회 · 바꾸기 · 고쳐 쓰기(초안 → [저장] 때 내 기계)
- 줄 쌓기 기계 줄 이름 단추 = 기계 창(왼쪽 찾기 · 오른쪽 공통 기계 양식 읽기 전용 · 지금 줄 = 사본 값 · 새 값 표시)
- [이 기계로 바꾸기] = repick 그대로(823-1 · 되돌리기) · M01 목록도 시간당(machine-rows)
- [고쳐 쓰기](개인 층 SK) = 서버 확인(쓰기 없음) → 문서 칸 내기계초안 · 줄 입력 개인 EQP초안-N
- [저장] 한 번에 서버가 초안 만들기 · 고침 → 키 바꿈 → 사본 박기 · 실패 = 만든 파일 지움 · 고친 줄 되돌림
- 미리 셈 = 초안을 그 요청 안만 셈 색인에 얹음 · 브라우저 = 셈 꼴 값을 캐시에
- 회사 · 마스터 층 · M01 기초 = 조회 · 바꾸기만(까닭 한 줄) · 옵션 가로 곁 [조회]
- 걷음: 줄 옆 「복사해 내 기계로」(line_stack_mine.ts) · 찾기 창 [새 기계 만들기] · 개인관리 › 기계 만들기 · 고치기(보기 · 쓰는 곳 · 지우기만)
- 내 기계 쓰는 곳 = 수식 길 사본도 셈
- 시험 test_827_1_machine_window(8)

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

214 lines
9.7 KiB
Python

"""M02 단가산출(SK) 수식 길 — 읽기 · 미리 셈 · 저장(PLAN 802-4a 4 · 요구 664) — 서버 단독.
M01 수식 문서 길과 같은 꼴: 읽기 {문서, 셈} · 미리 셈 {문서} → {셈} · 저장 {문서} → {문서, 셈}.
몸 칸 — `문서`(이 SK 수식 문서 · 키 = SK 키) · `끊음` {키: 문서}(이 사본에 굳힌 부름 · 고칠 수 있음 ·
없는 키 = 다시 이음) · `할일` 단가갱신 | 재생성 · `판`(저장 · 그 SK 파일 판).
셈 = 서버 정본(`M02_Doc_Copy.run`) — 화면이 보낸 값 · 사본은 안 받음(문서 · 끊음 글만).
저장 = 그 SK 저장(`store.write` — 층 쓰기 권한 · 판 409 · 옛 칸 · 로직 사본 그대로) ·
문서 고침 = 따로(이 문서가 정본) · 재생성 = 지금 M01 로 다시 찍어 파생으로(고친 문서 · 끊음 버림) ·
단가갱신 = 값 줄만 지금 M01(문서 그대로).
가져온 식 SK(807-1) = 가져온 줄 문서 · 셈 = 단가기준(엑셀 값 · 연결 값) · 부름 · 끊음 없음
(형제 SK 는 셈 때 읽음) · 단가갱신 = 문서 그대로 · 바탕 참조 SK = 값 사본 없음(400).
"""
from __future__ import annotations
import json
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Gap as gap
from M02_MasterTemplete import M02_Doc_Import as dim
from M02_MasterTemplete import M02_Doc_Screen as screen
from M02_MasterTemplete import M02_Doc_Sk as dsk
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine_Draft as draft
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
MAX = 2_000_000 # 문서 글 크기(바이트) 상한
JOBS = (skc.PRICE, skc.REGEN)
def _bad(why: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, why)
def _file(key: str) -> dict[str, Any]:
if not sd.KEY.match(str(key or "")):
raise _bad("키(SK-######)")
return store.read(sd.KIND, key)
def _settled(doc: dict, job: Any = None) -> tuple[dict, Any]:
"""옛 셈 길 첫머리(`calc_doc`)와 같게 — (정리한 문서, 사본 색인)."""
from M02_MasterTemplete import M02_Sangeun_Calc as calc
raw = {**doc, skc.JOB: job} if job else doc
whole = skc.over(doc := skc.settle(raw), skc.whole_()) # 내 기계(821-1)
try:
return sd.clean(doc, None, whole), whole
except sd.DocError as e:
raise calc._fail(e) from e
def current(doc: dict, job: Any = None) -> dict:
"""그 SK 의 지금 수식 사본(할일 적용 · 저장 없음) — 따로 = 저장한 문서 · 파생 = 옛 칸으로 지음."""
from M02_MasterTemplete import M02_Sangeun_Calc as calc
if not dsk.own(doc):
raise _bad("바탕 참조 단가산출 — 수식 사본 없음")
clean, whole = _settled(doc, job)
if not dc.has(clean): # 옮기기 전 · 새 SK — 저장 때 지을 사본을 미리
seed = dsk.settle(clean, whole, job, doc).get(dsk.KEY)
if seed is None:
raise _bad("수식 사본을 지을 수 없음 — 바탕 로직 · 줄 없음")
clean = {**clean, dsk.KEY: seed}
box: list = []
try:
calc.run(clean, whole, catch=box)
except sd.DocError as e:
raise calc._fail(e) from e
got = dsk.copy_of(clean, box[-1] if box else None)
return got if got is not None else clean[dsk.KEY]
def answer(copy: dict, sk: dict) -> dict[str, Any]:
"""{문서, 부름, 끊음, 셈, 사본 표지, 연결 기록} — 글 · 수는 JSON 꼴 · 가져온 식 표지 = 단가기준."""
got = dsk.run(sk, copy)
head = {k: copy.get(k) for k in ("시점", "단가시점")}
mark = {**head, "따로": bool(copy.get("따로"))}
mark |= (
{"단가기준": dim.basis_of(sk)}
if dim.is_imported(sk)
else {**dc.state(copy), "사본없음": len(gap.gaps(copy))} # 저장하면 지금 값으로 굳음
)
body = {
"문서": copy["문서"],
"부름": copy.get("부름") or {},
"끊음": copy.get("끊음") or {},
"셈": got,
"사본": mark,
"연결": dim.record(copy["문서"], sk) if dim.is_imported(sk) else screen.record(copy),
}
return json.loads(json.dumps(dc.ml._norm(body), ensure_ascii=False, default=str))
def read(key: str) -> dict[str, Any]:
"""읽기 — {문서, 부름, 끊음, 셈, 사본, 판}."""
got = _file(key)
return {**answer(current(got["문서"]), got["문서"]), "판": got["판"]}
# ── 몸 검사 ───────────────────────────────────────────────────────────
def _doc(raw: Any, key: str, where: str) -> dict:
if not isinstance(raw, dict):
raise _bad(f"{where} · 묶음 아님")
if len(json.dumps(raw, ensure_ascii=False, default=str).encode("utf-8")) > MAX:
raise _bad(f"{where} · 너무 큼")
for name in ("변수", "식", "금액"):
if not isinstance(raw.get(name, []), list):
raise _bad(f"{where} · 「{name}」 목록 아님")
return {**raw, "키": key}
def edited(copy: dict, body: dict, key: str, sk: dict | None = None) -> dict:
"""화면 고침 얹은 사본 — 문서 · 끊음 글만 받음(값 · 부름은 서버 것 · 가져온 식 = 부름 없음)."""
if body.get("할일") not in (None, "", *JOBS):
raise _bad("할일은 단가갱신 · 재생성")
if body.get("할일") == skc.REGEN:
return copy # 재생성 = 고친 것 버림(그 사본이 이미 새로 찍힌 것)
out = {**copy}
if body.get("문서") is not None:
out["문서"] = _doc(body["문서"], key, "문서")
cut = body.get("끊음")
if cut is not None:
if not isinstance(cut, dict):
raise _bad("끊음 · {키: 문서} 묶음")
keys = set(copy.get("부름") or {}) | set(copy.get("끊음") or {})
if bad := sorted(k for k in cut if k not in keys):
raise _bad(f"끊음 · 이 사본이 부르지 않는 단가산출 {bad}")
out["끊음"] = {k: _doc(v, k, f"끊음 「{k}」") for k, v in cut.items()}
calls = [
*dc.calls(out["문서"]),
*(c for d in out.get("끊음", {}).values() for c in dc.calls(d)),
]
if not dim.is_imported(sk):
out["부름"] = dc.closure(dc.over(out), calls, copy.get("부름"))
out = gap.fill(out) # 셈이 읽은 줄 가운데 사본 없는 것 = 지금 값으로 박음(822-1)
out = draft.renew(out) # 기계 창 초안으로 만든 · 고친 내 기계 = 새 값(827-1)
out["따로"] = True
return out
def preview(key: str, body: dict) -> dict[str, Any]:
"""미리 셈(저장 없음) — {셈, 전, 후, 바뀐자원?, 버림?} · 기계 창 초안(827-1) = 이 요청 안만 얹음."""
with draft.overlay(body.get("문서")):
return _preview(key, body)
def _preview(key: str, body: dict) -> dict[str, Any]:
doc = _file(key)["문서"]
mine = _basis(doc, body)
before = current(doc)
job = body.get("할일") or None
was = current(doc, job) if job else before
after = edited(was, body, key, doc)
out = answer(after, mine)
if not dim.is_imported(doc): # 고침으로 새로 읽은 줄 = 지금 값으로 셈 · 저장하면 굳음(826-3)
out["사본"]["사본없음"] += len(_rows(after) - _rows(was))
out |= {"전": dc.money(dsk.run(doc, before)), "후": dc.money(out["셈"])}
if job == skc.PRICE and not dim.is_imported(doc):
out["바뀐자원"] = dc.refresh(before)[1]
if job == skc.REGEN:
out["버림"] = {
"문서고침": bool(before.get("따로")),
"끊음": sorted(before.get("끊음") or {}),
}
return json.loads(json.dumps(dc.ml._norm(out), ensure_ascii=False, default=str))
def _rows(copy: dict) -> set[str]:
return {str(r.get("키")) for r in [*(copy.get("로직") or []), *(copy.get("단가") or [])]}
def _basis(doc: dict, body: dict) -> dict:
"""가져온 식 단가기준 바꿈(807-1 · 몸 `단가기준` 엑셀 | 우리) — 그 밖 SK · 빈 값 = 그대로."""
want = body.get("단가기준")
if not want or not dim.is_imported(doc):
return doc
if want not in ("엑셀", "우리"):
raise _bad("단가기준은 엑셀 · 우리")
return {**doc, "가져온": {**doc["가져온"], "단가기준": want}}
def save(key: str, body: dict) -> dict[str, Any]:
"""저장(층 쓰기) — 서버가 다시 셈한 사본을 그 SK 에 씀 · {문서, 부름, 끊음, 셈, 사본, 판} ·
기계 창 초안(827-1) = 같은 요청 안에서 내 기계 만들기 · 고침 → 키 바꿈 → 저장(실패 = 되돌림)."""
got = _file(key)
if str(body.get("판") or "") != got["판"]:
raise _bad({"stale": [key], "판": got["판"]}, 409)
return draft.saving(body, lambda b: _save(key, b, got))
def machine(key: str, no: str) -> dict[str, Any]:
"""기계 창(827-1) — 이 SK 사본에 박힌 기계 줄 · 지금 줄 · 새 값."""
doc = _file(key)["문서"]
copy = current(doc) if dsk.own(doc) and not dim.is_imported(doc) else None
return draft.copy_row(doc, copy, no)
def _save(key: str, body: dict, got: dict) -> dict[str, Any]:
job = body.get("할일") or None
raw = {**_basis(got["문서"], body), **({skc.JOB: job} if job else {})}
token = None
if job != skc.REGEN and (body.get("문서") is not None or body.get("끊음") is not None):
mine = edited(current(got["문서"], job), body, key, got["문서"])
token = dsk.SAVING.set({"키": key, "사본": mine})
try:
store.write(sd.KIND, key, got["판"], raw)
finally:
if token is not None:
dsk.SAVING.reset(token)
return read(key)