- 공용 화면 A00_Common/formula_doc_*(M01 · 뒤에 M02 = 길 앞머리만 바꿈) — 옛 수식 나무 화면은 M01 에서 걷음(M02 는 그대로) - 보이는 수식 그 자리 고침(공용 editEq · 전체 고르기 = 한 줄 글) · 분수 · 첨자 · 만약 = 탄 갈래만 · 반올림 자리 숨김 · 값 식 단위 - 값 식 숫자 누름 = 그 자리 값 · 기호 값 누름 = 정의 줄 · 정의 영역 = 기호 : 이름 — 연결(키 · 값 · 단위 없음 · 탄 갈래로 닿는 기호만) - 새 기호 = 정의 필요 → 숫자 · 선택 · 입력 · 요소 · 표 · 단가산출 · 식 연결 · 기호 · 첨자 · 이름 고침 · 정의 지움 - 금액 = 경쟁사 꼴 이름 : 단가 × 수량 = 금액 · 묶음 하나 = 계 · 둘 이상 = 가. 이름 소계 + 합계 · 단가산출 연결 펼침 = 그 문서 식 · 값 식 - 셈 = 브라우저 계산기 즉시 · 연결 값 = 서버 기록 캐시(common_util_formula_doc_cache.ts ↔ master_doc_record.py) · 없는 연결만 뒤에서 미리 셈 - 서버 길 GET /api/m01/logic/doc · POST preview · save(서버가 정본으로 다시 셈 · 수식 문서 파일에 씀) · links · GET table (M01_MasterData_Doc.py) - 연결 대상 안 고른 연결(표 · 요소 · 단가산출) = 연결 끊김 표지(파이썬 · TS 짝 가드 — 서버 500 걷음) - 시험 test_802_3_doc_screen 11(읽기 · 빈 문서 · 저장 = 서버 다시 셈 · 관리자 아니면 막힘 · 지움 견딤 · 표지 갈래 · 후보) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
"""M01 단가산출 기초 수식 문서 화면 서버 몫(PLAN 802-3 · 요구 664 · 요구서_664 3 장).
|
||
|
||
문서 = `resources/master_data/수식문서/수식_<로직 파일 꼬리>.json`(`master_doc_link.FOLDER`) ·
|
||
셈 = `master_doc_calc.calc`(파이썬 정본 · TS 짝 `common_util_formula_doc_calc.ts`) ·
|
||
연결 기록 = `master_doc_record.Rec` — 셈이 쓴 요소 값 · 표 · 단가산출 문서를 화면 캐시로 실음
|
||
(화면은 같은 계산기로 즉시 셈 · 캐시에 없는 연결만 뒤에서 미리 셈).
|
||
- 읽기 {키} → {문서, 셈, 연결, 파일, 새} — 문서가 없으면 빈 문서(이름 · 단위 = 로직 줄)
|
||
- 미리 셈 {문서, 입력} → {셈, 연결} — 쓰기 없음
|
||
- 저장 {문서} → {문서, 셈, 연결, 파일} — 서버가 정본으로 다시 셈 · 문서 파일에 씀(브라우저 셈 값 안 받음)
|
||
권한 = 라우터를 `main.py` 가 `system_admin_only` 로 붙임. M02 는 같은 꼴로 따로(802-4).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
from M01_MasterData import M01_MasterData_Store as store
|
||
|
||
import master_doc as md # noqa: E402 — `store` 가 scripts 를 길에 둔 뒤
|
||
import master_doc_calc as mc # noqa: E402
|
||
import master_doc_link as ml # noqa: E402
|
||
import master_doc_record as mr # noqa: E402
|
||
import master_equation_link as link # noqa: E402
|
||
|
||
OWN_FILE = "수식_자체.json"
|
||
_memo: dict = {}
|
||
|
||
|
||
def folder() -> Path:
|
||
return ml.FOLDER
|
||
|
||
|
||
def _stamp() -> tuple:
|
||
return tuple((p.name, p.stat().st_mtime_ns) for p in sorted(folder().glob("수식_*.json")))
|
||
|
||
|
||
def docs() -> dict[str, dict]:
|
||
"""저장 문서 전부 {키: 문서} — 파일 시각이 같으면 다시 안 읽음."""
|
||
stamp = (str(folder()), _stamp())
|
||
if _memo.get("stamp") != stamp:
|
||
_memo.update(stamp=stamp, docs=ml.load_all(folder()))
|
||
return _memo["docs"]
|
||
|
||
|
||
def _dec(doc) -> dict:
|
||
"""화면 JSON(float) → Decimal 문서 — 저장 문서와 같은 수 꼴로 셈."""
|
||
return json.loads(json.dumps(doc, ensure_ascii=False), parse_float=Decimal, parse_int=Decimal)
|
||
|
||
|
||
def _where(key: str) -> str | None:
|
||
for path in sorted(folder().glob("수식_*.json")):
|
||
if any(str(d.get("키")) == key for d in ml.load_file(path).get("줄") or []):
|
||
return path.name
|
||
return None
|
||
|
||
|
||
def _run(doc: dict, given: dict | None = None) -> dict:
|
||
whole = store.loaded()[1]
|
||
every = docs()
|
||
own = str(doc.get("키") or "")
|
||
links = ml.Links(whole, {**every, own: doc} if own else every)
|
||
rec = mr.Rec(links)
|
||
rec.prefetch(doc)
|
||
answer = mc.calc(doc, rec, _dec(given or {}))
|
||
return {"셈": answer, "연결": rec.out()}
|
||
|
||
|
||
def _check(doc) -> dict:
|
||
if not isinstance(doc, dict) or doc.get("꼴") != "수식문서":
|
||
raise store.StoreError(400, "수식 문서 꼴 아님")
|
||
if not str(doc.get("키") or "").strip():
|
||
raise store.StoreError(400, "문서 키 없음")
|
||
for k in ("변수", "식", "금액"):
|
||
if not isinstance(doc.get(k) or [], list):
|
||
raise store.StoreError(400, f"「{k}」 는 목록")
|
||
return _dec(doc)
|
||
|
||
|
||
def read(key: str) -> dict:
|
||
found = docs().get(key)
|
||
fresh = found is None
|
||
if fresh:
|
||
row = store.logic(key)["logic"] # 없는 키 = 404
|
||
found = md.blank(key, str(row.get("이름") or ""), str(row.get("결과단위") or "원"))
|
||
out = _run(found)
|
||
return {"문서": found, **out, "파일": _where(key) or "", "새": fresh}
|
||
|
||
|
||
def preview(doc: dict, given: dict | None = None) -> dict:
|
||
return _run(_check(doc), given)
|
||
|
||
|
||
def _file_for(key: str) -> str:
|
||
"""문서 파일 — 있던 파일 › 로직 파일 짝 › 자체."""
|
||
have = _where(key)
|
||
if have:
|
||
return have
|
||
try:
|
||
return ml.file_of(store.logic(key)["file"])
|
||
except store.StoreError:
|
||
return OWN_FILE
|
||
|
||
|
||
def save(doc: dict) -> dict:
|
||
doc = _check(doc)
|
||
key = str(doc["키"]).strip()
|
||
name = _file_for(key)
|
||
path = folder() / name
|
||
data = ml.load_file(path) if path.exists() else {"그룹": "수식문서", "로직파일": "", "줄": []}
|
||
rows = data.setdefault("줄", [])
|
||
at = next((i for i, d in enumerate(rows) if str(d.get("키")) == key), None)
|
||
if at is None:
|
||
rows.append(doc)
|
||
else:
|
||
rows[at] = doc
|
||
path.write_text(ml.dump(data), encoding="utf-8", newline="\n")
|
||
_memo.clear()
|
||
return {"문서": doc, **_run(doc), "파일": name}
|
||
|
||
|
||
def candidates(ask: dict) -> dict:
|
||
"""연결 후보 — 요소 · 표 = M01 색인 찾기 · 단가산출 = 수식 문서 이름 찾기(읽기만)."""
|
||
if ask.get("갈래") != "단가산출":
|
||
return link.candidates(store.loaded()[1], ask)
|
||
words = str(ask.get("검색") or "").lower().split()
|
||
out = []
|
||
for key, d in docs().items():
|
||
text = f"{key} {d.get('이름') or ''}".lower()
|
||
if words and all(w in text for w in words):
|
||
ins = [
|
||
str(v.get("열쇠"))
|
||
for v in d.get("변수") or []
|
||
if (v.get("연결") or {}).get("갈래") in ("선택", "입력", "숫자") and v.get("열쇠")
|
||
]
|
||
out.append({"키": key, "이름": d.get("이름"), "단위": d.get("결과단위"), "입력": ins})
|
||
return {"후보": out[: link.LIMIT], "더있음": len(out) > link.LIMIT}
|
||
|
||
|
||
def table(key: str) -> dict:
|
||
"""표 하나(조건 · 칸 고르기) — 기록 꼴 그대로."""
|
||
rec = mr.Rec(ml.Links(store.loaded()[1], {}))
|
||
rec.see_table(key)
|
||
return rec.표[key]
|