Files
Aislo/M01_MasterData/M01_MasterData_Doc.py
T
eomsangdonandClaude Opus 5.5 abe722f378 feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)
- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`)
- 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로)
- 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝
- 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김
- 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글
- 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음)
- 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음
- 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록)
- 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음)

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

200 lines
7.6 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]
# ── 로직 지우기(804-7 · 요구 687-1) — 로직 줄 + 수식 문서를 서버 정본에서 함께 ──
def users(key: str) -> list[str]:
"""이 로직을 부르는 곳 — 다른 수식 문서(단가산출 연결) · 일위대가 조합 · M02 단가산출(바탕).
다른 로직 줄이 부르는 것은 로직 저장 검사(422)가 막음."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
out = []
for k, d in docs().items():
if k != key and any(
(v.get("연결") or {}).get("갈래") == "단가산출"
and str((v.get("연결") or {}).get("키")) == key
for v in d.get("변수") or []
):
out.append(f"단가산출 {k} {d.get('이름') or ''}".strip())
for c in combo_store.combos_of_logic(key)["combos"]:
out.append(f"일위대가 조합 {c.get('키')} {c.get('이름') or ''}".strip())
for where, path in layers.layer_files("sangeun"):
try:
sk = json.loads(path.read_bytes())
except (OSError, ValueError):
continue
if str((sk.get("바탕") or {}).get("로직") or "") == key:
out.append(f"M02 단가산출 {where} {sk.get('이름') or path.stem}")
return out
def drop(key: str) -> None:
"""수식 문서 줄 하나 지움(없으면 그냥)."""
name = _where(key)
if not name:
return
path = folder() / name
data = ml.load_file(path)
data["줄"] = [d for d in data.get("줄") or [] if str(d.get("키")) != key]
path.write_text(ml.dump(data), encoding="utf-8", newline="\n")
_memo.clear()
def remove(key: str, file: str, version: str, confirmed: bool) -> dict:
"""로직 지우기 — 부르는 곳이 있으면 409 {쓰는곳} · 로직 저장 길(개정 확인 · 판 · 검사) 뒤 문서도."""
from M01_MasterData import M01_MasterData_Store_Make as make
used = users(key)
if used:
raise store.StoreError(409, {"쓰는곳": used})
got = make.save(
[{"file": file, "version": version, "changes": [{"op": "delete", "key": key}]}],
confirmed,
)
drop(key)
return got