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>
This commit is contained in:
2026-11-07 19:23:57 +09:00
co-authored by Claude Opus 5.5
parent d51c0b2200
commit 54fb55487c
15 changed files with 2799 additions and 13 deletions
@@ -285,7 +285,9 @@ class _Run:
return Stop(f"「{name}」 연결 갈래 「{kind}」", "연결 꼴", name)
def sub(self, payload: dict, name: str):
key = payload["키"]
key = str(payload.get("키") or "")
if not key: # 고르는 중 — 표지(예외 아님) · 짝 TS sub
raise Broken("단가산출 고르지 않음")
if key in self.stack or len(self.stack) > DEEP:
return Stop(f"단가산출 부름 돌고 돎 — {' → '.join([*self.stack, key])}", "순환", name)
doc = self.links.doc(key)
@@ -122,6 +122,8 @@ class Links:
raise mc.Broken(str(e)) from None
def table(self, p: dict):
if not p.get("표") or not p.get("칸"): # 고르는 중 — 표지(예외 아님)
raise mc.Broken("표 · 칸 고르지 않음")
table = self._get(p["표"])
if "줄" not in table or "조건" not in table:
raise mc.Broken(f"「{p['표']}」 표 아님")
@@ -135,6 +137,8 @@ class Links:
return _cell(row[p["칸"]], p["칸"])
def element(self, p: dict):
if not p.get("키") and p.get("재료") is None: # 고르는 중 — 표지(예외 아님)
raise mc.Broken("요소 고르지 않음")
env = {}
if p.get("지역") is not None:
env[mm.REGION] = p["지역"]
@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
"""수식 문서 연결 기록 — 화면 캐시로 싣는 연결 값(PLAN 802-3 · 꼴 `_수식문서_틀.md`).
서버 셈(`master_doc_calc.calc`)이 쓴 요소 값 · 표 · 단가산출 문서를 적어 화면에 실음 —
브라우저가 같은 계산기(`common_util_formula_doc_calc.ts`)로 네트워크 없이 다시 셈.
화면 짝 = `common_util/common_util_formula_doc_cache.ts`(기록 → `DocLinks`).
- 요소 = 부른 꼴(payload) 그대로 + {값 | 빈 | 끊김}
- 표 = 표 통째(조건 · 값칸 · 줄) — 옵션 · 입력이 바뀌어도 화면이 줄을 찾음 · 표 아님 = {끊김}
- 단가산출 = 문서 통째(없으면 null)
`prefetch` = 식 · 옵션과 무관하게 글로 적힌 표 · 단가산출 키를 미리 담음(구멍 `{…}` 든 키는 셈 때만).
"""
from __future__ import annotations
import master_doc_calc as mc
import master_formula as mf
class Rec:
"""`Links` 를 감싸 부른 연결을 적음 — `out()` = 화면 캐시."""
def __init__(self, links):
self.links = links
self.요소: dict[str, dict] = {}
self.표: dict[str, dict | None] = {}
self.문서: dict[str, dict | None] = {}
self.이름: dict[str, str] = {}
def element(self, p: dict):
key = repr(sorted(p.items(), key=lambda kv: kv[0]))
try:
got = self.links.element(p)
except mc.Gap as e:
self.요소[key] = {"p": p, "빈": str(e)}
raise
except mc.Broken as e:
self.요소[key] = {"p": p, "끊김": str(e)}
raise
self.요소[key] = {"p": p, "값": got}
return got
def see_table(self, ref: str) -> None:
if ref in self.표:
return
try:
table = self.links.master.get(ref)
except mf.FormulaError as e:
self.표[ref] = {"끊김": str(e)}
return
if "줄" not in table or "조건" not in table:
self.표[ref] = {"끊김": f"「{ref}」 표 아님"}
return
keep = ("키", "원문번호", "이름", "조건", "범위규칙", "값칸", "줄")
self.표[ref] = {k: table[k] for k in keep if k in table}
def table(self, p: dict):
self.see_table(str(p.get("표")))
return self.links.table(p)
def doc(self, key: str):
got = self.links.doc(key)
self.문서[key] = got
return got
def name(self, ref: str) -> None:
"""키 → 이름(화면 연결 글 · DB 키는 안 보임)."""
if not ref or "{" in ref or ref in self.이름:
return
doc = self.links.docs.get(ref)
if doc is not None:
self.이름[ref] = str(doc.get("이름") or "")
return
try:
row = self.links.master.get(ref)
except mf.FormulaError:
return
self.이름[ref] = " ".join(str(x) for x in (row.get("이름"), row.get("규격")) if x)
def prefetch(self, doc: dict, seen: set | None = None) -> None:
seen = set() if seen is None else seen
for v in doc.get("변수") or []:
link = v.get("연결") or {}
kind, ref = link.get("갈래"), str(link.get("키") or link.get("표") or "")
if kind in ("표", "요소", "단가산출"):
self.name(ref)
if not ref or "{" in ref:
continue
if kind == "표":
self.see_table(ref)
elif kind == "단가산출" and ref not in seen:
seen.add(ref)
sub = self.doc(ref)
if sub:
self.prefetch(sub, seen)
def out(self) -> dict:
return {
"요소": list(self.요소.values()),
"표": self.표,
"문서": self.문서,
"이름": self.이름,
}