- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/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>
200 lines
7.6 KiB
Python
200 lines
7.6 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]
|
||
|
||
|
||
# ── 로직 지우기(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
|