wip(m02-df): 서버 계산 M02_DF_Compute.py(재료 셋 · 다른 로직 · 선 규칙 · 끝수) · 설계 때 정할 것 · 다른 로직 값 길 · 시험 더함(규칙 하나 맞추는 중)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
2026-10-01 19:50:41 +09:00
co-authored by Claude Opus 5.5
parent 77d1bfbdc5
commit 4a62f05caf
4 changed files with 805 additions and 191 deletions
+41 -169
View File
@@ -25,21 +25,19 @@
from __future__ import annotations
import copy
import json
import os
import re
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from common_util import common_util_spreadsheet as spreadsheet
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData.M01_MasterData_Router import _num
from M02_MasterTemplete.DataFlow import M02_DF_Compute as dc
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
cm = store.cm
@@ -184,174 +182,19 @@ def user_edit(op: str, element: dict | None, uid: str | None) -> dict:
return {"element": one}
# ── 저장 · 계산 ────────────────────────────────────────────────────────
def _col(letters: str) -> int:
n = 0
for ch in letters:
n = n * 26 + ord(ch) - 64
return n
# ── 저장 · 계산(`M02_DF_Compute`) ─────────────────────────────────────
def _load(key: str) -> dict:
"""다른 로직 입력이 부르는 로직 — 내 것(`_data`) · 견본(`_sample`)."""
logic, _ = _read(key)
return {**logic, "key": key}
def _letters(n: int) -> str:
out = ""
while n:
n, rest = divmod(n - 1, 26)
out = chr(65 + rest) + out
return out
def _style(cells: dict, cell: str) -> dict:
return {"서식": cells[cell]["서식"]} if "서식" in (cells.get(cell) or {}) else {}
def _put(cells: dict, cell: str, value: Any) -> None:
"""받는 칸 하나 — 값 칸으로 덮어씀(수식이 있었어도) · 빈 값은 값만 뺌(서식은 둠)."""
keep = _style(cells, cell)
if value is None and not keep:
cells.pop(cell, None)
else:
cells[cell] = {**({"값": value} if value is not None else {}), **keep}
def _place(cells: dict, cell: str, value: Any) -> None:
"""값 하나 · 표 블록(cell 이 왼위) · 수식 `{"식": …}` 을 받는 칸에."""
m = _CELL.match(str(cell or "").upper())
if not m:
raise store.StoreError(400, f"칸 주소 모양이 아님 「{cell}」")
col, row, at = _col(m.group(1)), int(m.group(2)), f"{m.group(1)}{m.group(2)}"
if isinstance(value, dict) and "식" in value:
cells[at] = {**value, **_style(cells, at)}
elif isinstance(value, list):
for dy, line in enumerate(value):
for dx, one in enumerate(line):
_put(cells, f"{_letters(col + dx)}{row + dy}", one)
else:
_put(cells, at, value)
def _plain(v: Any) -> Any:
"""엔진 계산값 한 칸 → 수 · 글 · 참거짓 · 빈 값(오류는 None)."""
if not isinstance(v, dict):
return v
if "수" in v:
return Decimal(str(v["수"]))
if "글" in v:
return v["글"]
if "참" in v:
return v["참"]
return None
def _source(links: list[dict], box: str, port: str) -> dict | None:
return next((ln["from"] for ln in links if ln.get("to") == {"box": box, "name": port}), None)
def _input_ports(box: dict, local: list[dict], errors: list[str]) -> dict[str, Any]:
"""입력 상자 알약 값 — 요소 name · 표 「표」 · 변수 name(예시값)."""
inp, out = box.get("input") or {}, {}
if inp.get("mode") == "group":
group = inp.get("group")
asks = [
(el.get("name"), {"ref": el.get("ref"), "unit": el.get("unit")})
for el in inp.get("elements") or []
]
if inp.get("table"):
asks.append((TABLE_PORT, {"table": inp["table"]}))
for name, ask in asks:
got = dv.one_value({"group": group, **ask}, local)
out[name] = (got.get("rows") or got.get("value")) if got["ok"] else None
if not got["ok"]:
errors.append(f"「{box.get('name')} · {name}」 {got.get('reason')}")
for v in inp.get("variables") or []:
out[v.get("name")] = v.get("example")
return out
def compute(logic: dict, local: list[dict]) -> tuple[dict, dict]:
"""함수 상자 시트를 한 통합문서로 셈 → (값을 되돌려 넣은 다이어그램, 계산 결과)."""
boxes = {b.get("id"): b for b in logic.get("boxes") or []}
links = logic.get("links") or []
errors: list[str] = []
ports = {
i: _input_ports(b, local, errors) for i, b in boxes.items() if b.get("kind") == "input"
}
funcs = [b for b in boxes.values() if b.get("kind") == "function" and b.get("func")]
sheets, fills = [], []
for b in funcs: # 한 통합문서 — 시트 id · 이름 = 상자 id
f = b["func"]
sheet = {**copy.deepcopy(f.get("sheet") or {}), "id": b["id"], "이름": b["id"]}
cells = sheet.setdefault("칸", {})
for v in f.get("variables") or []:
if v.get("cell"):
_place(cells, v["cell"], v.get("example"))
for p in f.get("inputs") or []:
src = _source(links, b["id"], p.get("port"))
if src is None or src.get("box") not in boxes:
continue
up = boxes[src["box"]]
if up.get("kind") == "function":
at = ((up.get("func") or {}).get("output") or {}).get("cell") or ""
_place(cells, p["cell"], {"식": f"'{up['id']}'!{at}"})
fills.append((b["id"], p["cell"], ("ref", up["id"], at)))
else:
value = ports.get(up["id"], {}).get(src.get("name"))
_place(cells, p["cell"], value)
fills.append((b["id"], p["cell"], ("value", value, None)))
sheets.append(sheet)
top = [c.get("서식", 0) for s in sheets for c in s["칸"].values() if isinstance(c, dict)]
book = _num(
{"종류": "통합문서", "판": 1, "서식": [{}] * (int(max(top, default=0)) + 1), "시트": sheets}
)
try:
calc = spreadsheet.recalc_workbooks([book])[0] if sheets else {}
except spreadsheet.RecalcError as e:
raise store.StoreError(503, str(e)) from e
def cell_value(box_id: str, cell: str) -> Any:
hit = (calc.get(box_id) or {}).get(cell)
if hit is None:
sheet = next(s for s in book["시트"] if s["id"] == box_id)
return (sheet["칸"].get(cell) or {}).get("값")
if isinstance(hit, dict) and "오류" in hit:
errors.append(f"「{boxes[box_id].get('name')}」 {cell} 칸 계산 오류 {hit.get('오류')}")
return _plain(hit)
for b in funcs:
out = b["func"].get("output") or {}
ports[b["id"]] = {out.get("name"): cell_value(b["id"], out.get("cell") or "")}
outputs, cost = {}, {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}}
for b in boxes.values(): # 출력 상자 · 원가
if b.get("kind") != "output":
continue
src = _source(links, b["id"], OUTPUT_PORT)
value = ports.get(src["box"], {}).get(src.get("name")) if src else None
outputs[b["id"]] = value
kind = (b.get("output") or {}).get("cost")
if (
kind in COSTS
and isinstance(value, (int, float, Decimal))
and not isinstance(value, bool)
):
cost[kind] += Decimal(str(value))
elif kind == "값":
cost["값들"][b.get("name")] = value
cost["계"] = sum(cost[k] for k in COSTS)
done = copy.deepcopy(logic) # 받는 칸에 값만 되돌려 넣음 — 시트 사이 참조는 셈한 값으로
for b in done.get("boxes") or []:
if b.get("kind") != "function" or not b.get("func"):
continue
cells = b["func"].setdefault("sheet", {}).setdefault("칸", {})
for v in b["func"].get("variables") or []:
if v.get("cell"):
_place(cells, v["cell"], v.get("example"))
for box_id, cell, (how, a, at) in fills:
if box_id == b["id"]:
_place(cells, cell, cell_value(a, at) if how == "ref" else a)
return done, {"ports": ports, "outputs": outputs, "cost": cost, "errors": errors}
def _local(logic: dict) -> list[dict]:
return [*(logic.get("userElements") or []), *_users()]
def save(logic: dict) -> dict:
"""저장 — 입력 값 다시 받기 → 함수 상자 시트를 Node 로 셈 → 값만 되돌려 넣어 `_data`."""
"""저장 — 입력 값 다시 받기 → 함수 상자 시트를 Node 로 셈 → 받는 칸에 값만 되돌려 `_data`."""
if not str(logic.get("name") or "").strip():
raise store.StoreError(400, "이름을 넣어 주세요")
if not isinstance(logic.get("boxes"), list) or not isinstance(logic.get("links"), list):
@@ -361,13 +204,27 @@ def save(logic: dict) -> dict:
key = None # 견본은 그대로 두고 새 번호로
elif key:
_read(key) # 있는 것만 고침 — 없으면 404
done, result = compute(logic, [*(logic.get("userElements") or []), *_users()])
got = dc.run(logic, {}, _load, _local(logic), [key or ""])
if not key:
used = [int(m.group(1)) for p in DATA.glob("DF-*.json") if (m := _KEY.match(p.stem))]
key = f"DF-{max(used, default=0) + 1:04d}"
done = {**done, "key": key, "saved": datetime.now().isoformat(timespec="seconds")}
done = {**got.pop("filled"), "key": key, "saved": datetime.now().isoformat(timespec="seconds")}
_write(DATA / f"{key}.json", _num(done))
return {"key": key, "logic": done, "values": result}
return {"key": key, "logic": done, "values": got}
def logic_value(key: str, designs: dict) -> dict:
"""다른 로직 값 — 저장 로직 · 견본을 받은 설계 값(없으면 예시값)으로 셈 · 출력 이름 → 값."""
other = _load(key)
got = dc.run(other, dc._design_values(other, designs), _load, _local(other), [key])
names = {b["id"]: b.get("name") for b in other.get("boxes") or []}
outputs = {names[i]: v for i, v in got["outputs"].items() if i in names}
return {"key": key, "outputs": outputs, "cost": got["cost"], "errors": got["errors"]}
def design_inputs(key: str) -> dict:
"""설계 때 정할 것 — 변수 · 재료 자리 · 다른 로직 받는 알약(선이 없을 때) · CfDesign 꼴."""
return {"key": key, "designs": dc.design_inputs(_load(key))}
# ── 길 ────────────────────────────────────────────────────────────────
@@ -417,6 +274,21 @@ def post_user_elements(body: UserBody) -> dict:
STANDARD = HERE / "_standard"
class LogicValueBody(BaseModel):
key: str
designs: dict[str, Any] = {}
@router.post("/logic-value")
def post_logic_value(body: LogicValueBody) -> dict:
return _call(logic_value, body.key, body.designs)
@router.get("/design-inputs")
def get_design_inputs(key: str) -> dict:
return _call(design_inputs, key)
class StandardsBody(BaseModel):
unit: dict[str, Any]
money: dict[str, Any]