Files
Aislo/M01_MasterData/M01_MasterData_Doc.py
T
eomsangdonandClaude Opus 5.5 54fb55487c feat(802-3): M01 단가산출 기초 수식 문서 화면 — 왼쪽 기호 식 · 오른쪽 값 식 · 빈 문서 시작 · 연결 고르기
- 공용 화면 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>
2026-11-07 19:23:57 +09:00

146 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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]