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:
@@ -0,0 +1,518 @@
|
||||
"""단가산출 흐름도 서버 계산(PLAN 35-9 · 정함 4 · 5 · 6 · 7 · 12) — 저장 다시 셈 · 다른 로직 값.
|
||||
|
||||
모양 = sub7 Model `M02_DF_Model.ts`
|
||||
(CfLogic · CfLink.rule · CfElementRef.mode · CfInput.logic · CfRound).
|
||||
화면 계산(`M02_DF_Eval.evalLogic`)과 같은 차례 · 같은 엔진 — 서버는 함수 상자 시트들을 한 통합문서로
|
||||
묶어(시트 id = 상자 id) 우리 스프레드시트 엔진(Node · `common_util_spreadsheet`)으로 한 번에 셈.
|
||||
· 입력 상자 알약 = 요소(재료는 고정 · 선택 · 조건 찾기) · 표 · 변수 · 다른 로직 출력 —
|
||||
단가는 마스터에서 새로(`M02_DF_Values`) · 설계 값 values = {상자 id: {알약: 값}} 이 이김
|
||||
(재료 선택 자리는 고른 품목 키 · 수)
|
||||
· 선 → 받는 칸: 값 · 표 블록(왼위) · 앞 함수는 시트 사이 참조 · 선 규칙은 받는 칸의 엑셀 수식
|
||||
(round = ROUND · ROUNDUP · ROUNDDOWN · scale = × ÷ +% −% · gate = IF(조건, 값, "") ·
|
||||
lookup = 보조 시트에 표 블록 → COUNTIFS 로 하나인지 · SUMIFS 로 꺼냄 ·
|
||||
범위 칸 「·아래」 이상 · 「·위」 이하)
|
||||
· 함수 출력 · 출력 상자 끝수 = 보조 시트 ROUND 계열 · 원가 = 출력 상자 비용 종류별 합
|
||||
· 다른 로직 = 저장 로직 · 견본을 받은 설계 값(없으면 그 로직 예시값)으로 셈 · 서로 부르면 막음
|
||||
마스터 파일은 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable
|
||||
|
||||
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_Values as dv
|
||||
|
||||
_CELL = re.compile(r"^([A-Z]+)(\d+)$")
|
||||
TABLE_PORT, OUTPUT_PORT = "표", "값"
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
GATE = {"=": "=", "≠": "<>", "≥": ">=", "≤": "<=", ">": ">", "<": "<"}
|
||||
ROUND = {"반올림": "ROUND", "올림": "ROUNDUP", "버림": "ROUNDDOWN"}
|
||||
HELP = "_df_" # 보조 시트 이름 머리(표 블록 · 끝수)
|
||||
FAR = Decimal("1E+300") # 범위 칸 열린 끝 — 엑셀 비교가 늘 참이 되게
|
||||
Loader = Callable[[str], dict]
|
||||
|
||||
|
||||
def _col(letters: str) -> int:
|
||||
n = 0
|
||||
for ch in letters:
|
||||
n = n * 26 + ord(ch) - 64
|
||||
return n
|
||||
|
||||
|
||||
def _letters(n: int) -> str:
|
||||
out = ""
|
||||
while n:
|
||||
n, rest = divmod(n - 1, 26)
|
||||
out = chr(65 + rest) + out
|
||||
return out
|
||||
|
||||
|
||||
def _at(cell: str) -> tuple[int, int, str]:
|
||||
m = _CELL.match(str(cell or "").strip().upper())
|
||||
if not m:
|
||||
raise store.StoreError(400, f"칸 주소 모양이 아님 「{cell}」")
|
||||
return _col(m.group(1)), int(m.group(2)), f"{m.group(1)}{m.group(2)}"
|
||||
|
||||
|
||||
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 isinstance(value, dict) and "식" in value:
|
||||
cells[cell] = {"식": value["식"], **keep}
|
||||
elif value is None or value == "":
|
||||
cells.pop(cell, None) if not keep else cells.__setitem__(cell, keep)
|
||||
else:
|
||||
cells[cell] = {"값": value, **keep}
|
||||
|
||||
|
||||
def _place(cells: dict, cell: str, value: Any) -> None:
|
||||
"""받는 칸에 값 · 수식 · 표 블록(cell 이 왼위)."""
|
||||
col, row, at = _at(cell)
|
||||
if 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["수"]))
|
||||
return v.get("글", v.get("참"))
|
||||
|
||||
|
||||
def _literal(v: Any) -> str:
|
||||
"""수식 안 값 글 — 수는 그대로 · 글은 따옴표 · 빈 값은 빈 글."""
|
||||
if v is None:
|
||||
return '""'
|
||||
if isinstance(v, bool):
|
||||
return "TRUE" if v else "FALSE"
|
||||
if isinstance(v, (int, float, Decimal)):
|
||||
return str(v)
|
||||
return '"' + str(v).replace('"', '""') + '"'
|
||||
|
||||
|
||||
# ── 입력 상자 ──────────────────────────────────────────────────────────
|
||||
def _element_value(
|
||||
group: str, el: dict, given: Any, ports: dict, errors: list, box: dict, ctx: dict
|
||||
) -> Any:
|
||||
"""요소 알약 값 — 재료는 방식 셋 · 그 밖은 그룹 값 · 설계 값이 이김(선택 자리 = 키 · 수)."""
|
||||
mode = el.get("mode") or "fixed"
|
||||
if group == "재료" and mode == "choice":
|
||||
if isinstance(given, (int, float, Decimal)) and not isinstance(given, bool):
|
||||
return given
|
||||
got = dv.slot_price({"mode": "choice", "default": el.get("default")}, pick=given or None)
|
||||
elif group == "재료" and mode == "rule":
|
||||
rule = el.get("rule") or {}
|
||||
src = rule.get("from") or {}
|
||||
slot = {
|
||||
"mode": "rule",
|
||||
"scope": el.get("scope"),
|
||||
"field": rule.get("field"),
|
||||
"op": rule.get("op"),
|
||||
}
|
||||
got = dv.slot_price(slot, ports.get(src.get("box"), {}).get(src.get("name")))
|
||||
if not got["ok"] and el.get("default"):
|
||||
errors.append(
|
||||
f"「{box.get('name')} · {el.get('name')}」 {got['reason']} — 기본 품목으로 셈"
|
||||
)
|
||||
got = dv.slot_price({"mode": "fixed", "ref": el["default"]})
|
||||
elif given is not None:
|
||||
return given
|
||||
else:
|
||||
got = dv.one_value(
|
||||
{"group": group, "ref": el.get("ref"), "unit": el.get("unit")}, ctx["local"]
|
||||
)
|
||||
if not got["ok"]:
|
||||
errors.append(f"「{box.get('name')} · {el.get('name')}」 {got.get('reason')}")
|
||||
return None
|
||||
return got.get("value")
|
||||
|
||||
|
||||
def _input_ports(lg: dict, box: dict, values: dict, ports: dict, errors: list, ctx: dict) -> dict:
|
||||
inp, given, out = box.get("input") or {}, values.get(box["id"]) or {}, {}
|
||||
mode = inp.get("mode")
|
||||
if mode == "logic":
|
||||
return _logic_ports(lg, box, given, ports, errors, ctx)
|
||||
if mode == "group":
|
||||
group = inp.get("group")
|
||||
for el in inp.get("elements") or []:
|
||||
out[el.get("name")] = _element_value(
|
||||
group, el, given.get(el.get("name")), ports, errors, box, ctx
|
||||
)
|
||||
if inp.get("table"):
|
||||
got = dv.one_value({"group": group, "table": inp["table"]}, ctx["local"])
|
||||
out[TABLE_PORT] = got.get("rows") if got["ok"] else None
|
||||
if not got["ok"]:
|
||||
errors.append(f"「{box.get('name')} · 표」 {got.get('reason')}")
|
||||
for v in inp.get("variables") or []:
|
||||
out[v.get("name")] = given.get(v.get("name"), v.get("example"))
|
||||
return out
|
||||
|
||||
|
||||
def _logic_ports(lg: dict, box: dict, given: dict, ports: dict, errors: list, ctx: dict) -> dict:
|
||||
"""다른 로직 — 받는 알약(설계 값) = 선 · 검증 칸 값 · 없으면 그 로직 예시값 → 그 로직 출력."""
|
||||
ref = (box.get("input") or {}).get("logic") or {}
|
||||
key = str(ref.get("key") or "")
|
||||
if key in ctx["stack"]:
|
||||
errors.append(
|
||||
f"「{box.get('name')}」 로직이 서로를 부름 — {' → '.join([*ctx['stack'], key])}"
|
||||
)
|
||||
return {}
|
||||
try:
|
||||
other = ctx["load"](key)
|
||||
except store.StoreError as e:
|
||||
errors.append(f"「{box.get('name')}」 {e.detail}")
|
||||
return {}
|
||||
designs = {**given}
|
||||
for link in lg.get("links") or []:
|
||||
if link.get("to", {}).get("box") == box["id"]:
|
||||
src = link.get("from") or {}
|
||||
designs[link["to"].get("name")] = ports.get(src.get("box"), {}).get(src.get("name"))
|
||||
stack = [*ctx["stack"], key]
|
||||
result = run(other, _design_values(other, designs), ctx["load"], ctx["local"], stack)
|
||||
errors += [f"「{box.get('name')}」 {x}" for x in result["errors"]]
|
||||
names = {b["id"]: b.get("name") for b in other.get("boxes") or []}
|
||||
return {names[i]: v for i, v in result["outputs"].items() if i in names}
|
||||
|
||||
|
||||
def _design_values(lg: dict, designs: dict) -> dict:
|
||||
"""다른 로직의 설계 값 {알약 이름: 값} → 그 로직의 values {상자 id: {알약: 값}}."""
|
||||
out: dict[str, dict] = {}
|
||||
for d in design_inputs(lg):
|
||||
if d["port"] in designs and designs[d["port"]] is not None:
|
||||
out.setdefault(d["box"], {})[d["port"]] = designs[d["port"]]
|
||||
return out
|
||||
|
||||
|
||||
# ── 설계 때 정할 것 ────────────────────────────────────────────────────
|
||||
def design_inputs(lg: dict) -> list[dict]:
|
||||
"""변수 · 재료 자리(선택) · 다른 로직의 받는 알약(선이 없을 때) — Model `CfDesign` 꼴."""
|
||||
links = lg.get("links") or []
|
||||
fed = {
|
||||
(ln.get("to") or {}).get("box", "") + "\0" + (ln.get("to") or {}).get("name", "")
|
||||
for ln in links
|
||||
}
|
||||
out = []
|
||||
for box in lg.get("boxes") or []:
|
||||
inp = box.get("input") or {}
|
||||
if box.get("kind") != "input":
|
||||
for v in (box.get("func") or {}).get("variables") or []:
|
||||
out.append(
|
||||
{
|
||||
"box": box["id"],
|
||||
"port": v.get("name"),
|
||||
"kind": "변수",
|
||||
"unit": v.get("unit") or "-",
|
||||
"variable": v,
|
||||
}
|
||||
)
|
||||
continue
|
||||
for v in inp.get("variables") or []:
|
||||
out.append(
|
||||
{
|
||||
"box": box["id"],
|
||||
"port": v.get("name"),
|
||||
"kind": "변수",
|
||||
"unit": v.get("unit") or "-",
|
||||
"variable": v,
|
||||
}
|
||||
)
|
||||
for el in inp.get("elements") or [] if inp.get("group") == "재료" else []:
|
||||
if el.get("mode") == "choice":
|
||||
out.append(
|
||||
{
|
||||
"box": box["id"],
|
||||
"port": el.get("name"),
|
||||
"kind": "재료 자리",
|
||||
"unit": el.get("unit") or "-",
|
||||
"element": el,
|
||||
}
|
||||
)
|
||||
for pill in (
|
||||
((inp.get("logic") or {}).get("designs") or []) if inp.get("mode") == "logic" else []
|
||||
):
|
||||
if box["id"] + "\0" + pill.get("name", "") not in fed:
|
||||
out.append(
|
||||
{
|
||||
"box": box["id"],
|
||||
"port": pill.get("name"),
|
||||
"kind": "다른 로직",
|
||||
"unit": pill.get("unit") or "-",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── 선 규칙 → 받는 칸 수식 ─────────────────────────────────────────────
|
||||
class _Book:
|
||||
"""한 통합문서 — 함수 시트(id = 상자 id) + 보조 시트(표 블록 · 끝수)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sheets: list[dict] = []
|
||||
self.helpers: dict[str, dict] = {}
|
||||
|
||||
def helper(self, name: str) -> dict:
|
||||
if name not in self.helpers:
|
||||
self.helpers[name] = {"id": name, "이름": name, "칸": {}}
|
||||
self.sheets.append(self.helpers[name])
|
||||
return self.helpers[name]
|
||||
|
||||
|
||||
def _ref(sheet: str, cell: str) -> str:
|
||||
return f"'{sheet}'!{cell}"
|
||||
|
||||
|
||||
def _lookup(book: _Book, link: dict, block: Any, rule: dict, expr: Callable) -> Any:
|
||||
"""표에서 찾기 — 보조 시트에 표 블록(열린 범위 끝 ±1E300) · 하나면 그 칸 · 아니면 까닭 글."""
|
||||
if not isinstance(block, list) or not block:
|
||||
return {"식": '"표 없음"'}
|
||||
head = [str(h) for h in block[0]]
|
||||
sheet = book.helper(f"{HELP}{link['id']}")
|
||||
for y, line in enumerate(block, start=1):
|
||||
for x, v in enumerate(line, start=1):
|
||||
name = head[x - 1]
|
||||
if (
|
||||
y > 1
|
||||
and v is None
|
||||
and (name.endswith(dv.RANGE_LOW) or name.endswith(dv.RANGE_HIGH))
|
||||
):
|
||||
v = -FAR if name.endswith(dv.RANGE_LOW) else FAR
|
||||
_put(sheet["칸"], f"{_letters(x)}{y}", v)
|
||||
last = len(block)
|
||||
|
||||
def rng(name: str) -> str:
|
||||
c = _letters(head.index(name) + 1)
|
||||
return f"'{sheet['id']}'!{c}2:{c}{last}"
|
||||
|
||||
crit = []
|
||||
for cond in rule.get("conds") or []:
|
||||
col, value = str(cond.get("col")), expr(cond.get("from") or {})
|
||||
if col in head:
|
||||
crit.append(f"{rng(col)},{value}")
|
||||
elif col + dv.RANGE_LOW in head and col + dv.RANGE_HIGH in head:
|
||||
crit.append(
|
||||
f'{rng(col + dv.RANGE_LOW)},"<="&{value},{rng(col + dv.RANGE_HIGH)},">="&{value}'
|
||||
)
|
||||
else:
|
||||
return {"식": f'"표에 「{col}」 칸 없음"'}
|
||||
want = str(rule.get("col"))
|
||||
if want not in head or not crit:
|
||||
return {"식": f'"표에 「{want}」 칸 없음"'}
|
||||
joined = ",".join(crit)
|
||||
return {
|
||||
"식": f"IF(COUNTIFS({joined})=1,SUMIFS({rng(want)},{joined}),"
|
||||
f'IF(COUNTIFS({joined})=0,"표에 없는 조건","표에 여럿 맞음"))'
|
||||
}
|
||||
|
||||
|
||||
def _apply(rule: dict | None, src: str, round_: Callable) -> str:
|
||||
"""값 수식 src 에 규칙(round · scale · gate) — lookup 은 따로."""
|
||||
kind = (rule or {}).get("kind")
|
||||
if kind == "round":
|
||||
return round_(src, rule)
|
||||
if kind == "scale":
|
||||
v = _literal(rule.get("value"))
|
||||
return {
|
||||
"×": f"({src})*{v}",
|
||||
"÷": f"({src})/{v}",
|
||||
"+%": f"({src})*(1+{v}/100)",
|
||||
"-%": f"({src})*(1-{v}/100)",
|
||||
}.get(rule.get("op"), src)
|
||||
return src
|
||||
|
||||
|
||||
def _round(src: str, r: dict) -> str:
|
||||
return f"{ROUND.get(r.get('method'), 'ROUND')}({src},{int(r.get('places') or 0)})"
|
||||
|
||||
|
||||
# ── 계산 ───────────────────────────────────────────────────────────────
|
||||
def _order(lg: dict) -> tuple[list[dict], list[dict]]:
|
||||
"""함수 상자 차례(Eval `order` 와 같음) — 앞 함수가 먼저 · 돌고 도는 상자는 따로."""
|
||||
funcs = [b for b in lg.get("boxes") or [] if b.get("kind") == "function" and b.get("func")]
|
||||
links, done, left = lg.get("links") or [], [], {f["id"] for f in funcs}
|
||||
moved = True
|
||||
while moved and left:
|
||||
moved = False
|
||||
for f in funcs:
|
||||
if f["id"] in left and not any(
|
||||
ln["to"].get("box") == f["id"] and ln["from"].get("box") in left for ln in links
|
||||
):
|
||||
done.append(f)
|
||||
left.discard(f["id"])
|
||||
moved = True
|
||||
return done, [f for f in funcs if f["id"] in left]
|
||||
|
||||
|
||||
def _gate(rule: dict, src: str, expr: Callable) -> str:
|
||||
cond = (
|
||||
expr(rule.get("from") or {}) + GATE.get(rule.get("op"), "=") + _literal(rule.get("value"))
|
||||
)
|
||||
return f'IF({cond},{src},"")'
|
||||
|
||||
|
||||
def _build(lg: dict, funcs: list[dict], ports: dict, values: dict) -> tuple[_Book, list, dict]:
|
||||
"""(통합문서, 받는 칸 목록 [(상자, 칸, 표인가, 넣은 것)], 출력 자리 {상자: (시트, 칸)})."""
|
||||
boxes = {b["id"]: b for b in lg.get("boxes") or []}
|
||||
links = lg.get("links") or []
|
||||
book, fills, outs = _Book(), [], {}
|
||||
|
||||
def expr(port: dict) -> str:
|
||||
b = boxes.get(port.get("box"))
|
||||
if b is None:
|
||||
return '""'
|
||||
if b.get("kind") == "function":
|
||||
return _ref(*outs[b["id"]]) if b["id"] in outs else '""'
|
||||
v = ports.get(b["id"], {}).get(port.get("name"))
|
||||
return '""' if isinstance(v, list) else _literal(v)
|
||||
|
||||
def incoming(link: dict) -> Any:
|
||||
"""선 하나가 받는 칸에 넣을 것 — 값 · 표 블록 · 수식."""
|
||||
src, rule = link["from"], link.get("rule") or {}
|
||||
up = boxes.get(src.get("box")) or {}
|
||||
func = up.get("kind") == "function"
|
||||
raw = None if func else ports.get(up.get("id"), {}).get(src.get("name"))
|
||||
if rule.get("kind") == "lookup":
|
||||
return _lookup(book, link, raw, rule, expr)
|
||||
if isinstance(raw, list):
|
||||
return raw # 표 블록 — 규칙 없이 그대로 펼침
|
||||
if not rule and not func:
|
||||
return raw
|
||||
if rule.get("kind") == "gate":
|
||||
return {"식": _gate(rule, expr(src), expr)}
|
||||
return {"식": _apply(rule, expr(src), _round)}
|
||||
|
||||
def to(box: str, port: str) -> dict | None:
|
||||
return next((ln for ln in links if ln.get("to") == {"box": box, "name": port}), None)
|
||||
|
||||
for f in funcs:
|
||||
func = f["func"]
|
||||
sheet = {**copy.deepcopy(func.get("sheet") or {}), "id": f["id"], "이름": f["id"]}
|
||||
cells, given = sheet.setdefault("칸", {}), values.get(f["id"]) or {}
|
||||
for v in func.get("variables") or []:
|
||||
if v.get("cell"):
|
||||
_place(cells, v["cell"], given.get(v.get("name"), v.get("example")))
|
||||
for p in func.get("inputs") or []:
|
||||
if not str(p.get("cell") or "").strip():
|
||||
continue
|
||||
link = to(f["id"], p.get("port"))
|
||||
value = incoming(link) if link else given.get(p.get("port"))
|
||||
_place(cells, p["cell"], value)
|
||||
fills.append((f["id"], _at(p["cell"])[2], isinstance(value, list), value))
|
||||
book.sheets.append(sheet)
|
||||
out = func.get("output") or {}
|
||||
cell = _at(out.get("cell") or "A1")[2]
|
||||
if out.get("round"): # 함수 출력 끝수 — 보조 시트 칸 · 뒤 상자는 이 칸을 봄
|
||||
helper = book.helper(f"{HELP}r")
|
||||
at = f"A{len(helper['칸']) + 1}"
|
||||
_put(helper["칸"], at, {"식": _round(_ref(f["id"], cell), out["round"])})
|
||||
outs[f["id"]] = (helper["id"], at)
|
||||
else:
|
||||
outs[f["id"]] = (f["id"], cell)
|
||||
for b in boxes.values(): # 출력 상자 — 받은 값 · 규칙 · 끝수를 보조 시트 칸에
|
||||
link = to(b["id"], OUTPUT_PORT) if b.get("kind") == "output" else None
|
||||
if link is None:
|
||||
continue
|
||||
value = incoming(link)
|
||||
src = value["식"] if isinstance(value, dict) else _literal(value)
|
||||
if (b.get("output") or {}).get("round"):
|
||||
src = _round(src, b["output"]["round"])
|
||||
helper = book.helper(f"{HELP}o")
|
||||
at = f"A{len(helper['칸']) + 1}"
|
||||
_put(helper["칸"], at, {"식": src})
|
||||
outs["out:" + b["id"]] = (helper["id"], at)
|
||||
return book, fills, outs
|
||||
|
||||
|
||||
def _needs_func(boxes: dict) -> bool:
|
||||
"""재료 조건 찾기가 함수 출력을 쓰나 — 그러면 한 번 더 셈."""
|
||||
for b in boxes.values():
|
||||
for el in (b.get("input") or {}).get("elements") or []:
|
||||
src = (el.get("rule") or {}).get("from") or {}
|
||||
if el.get("mode") == "rule" and boxes.get(src.get("box"), {}).get("kind") == "function":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict:
|
||||
"""셈 — {ports, outputs, cost, errors, filled}(filled = 받는 칸에 값만 되돌려 넣음)."""
|
||||
ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""]}
|
||||
boxes = {b["id"]: b for b in lg.get("boxes") or []}
|
||||
funcs, loop = _order(lg)
|
||||
ports: dict[str, dict] = {}
|
||||
for _ in range(2 if _needs_func(boxes) else 1):
|
||||
errs = [f"「{f.get('name')}」 함수 상자가 서로를 기다림(돌고 도는 선)" for f in loop]
|
||||
for b in boxes.values():
|
||||
if b.get("kind") == "input":
|
||||
ports[b["id"]] = _input_ports(lg, b, values, ports, errs, ctx)
|
||||
book, fills, outs = _build(lg, funcs, ports, values)
|
||||
doc = _num({"종류": "통합문서", "판": 1, "서식": [{}] * 64, "시트": book.sheets})
|
||||
try:
|
||||
calc = spreadsheet.recalc_workbooks([doc])[0] if book.sheets else {}
|
||||
except spreadsheet.RecalcError as e:
|
||||
raise store.StoreError(503, str(e)) from e
|
||||
sheets = {s["id"]: s for s in book.sheets}
|
||||
|
||||
def read(sheet: str, cell: str) -> Any:
|
||||
hit = (calc.get(sheet) or {}).get(cell)
|
||||
if hit is not None:
|
||||
return _plain(hit)
|
||||
return ((sheets.get(sheet) or {"칸": {}})["칸"].get(cell) or {}).get("값")
|
||||
|
||||
for f in funcs:
|
||||
ports[f["id"]] = {f["func"]["output"].get("name"): read(*outs[f["id"]])}
|
||||
errors = errs
|
||||
for sheet, cells in calc.items():
|
||||
for cell, v in cells.items():
|
||||
if "오류" in v:
|
||||
name = boxes.get(sheet, {}).get("name") or "끝수 · 규칙"
|
||||
errors.append(f"「{name}」 {cell} 칸 계산 오류 {v['오류']}")
|
||||
elif v.get("글") in ("표에 없는 조건", "표에 여럿 맞음", "표 없음"):
|
||||
errors.append(f"표에서 찾기 — {v['글']}")
|
||||
outputs, cost = {}, {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}}
|
||||
for b in boxes.values():
|
||||
if b.get("kind") != "output":
|
||||
continue
|
||||
at = outs.get("out:" + b["id"])
|
||||
value = read(*at) if at else None
|
||||
value = None if value == "" else value
|
||||
outputs[b["id"]] = value
|
||||
kind = (b.get("output") or {}).get("cost") or "값"
|
||||
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
|
||||
elif value is not None:
|
||||
errors.append(f"「{b.get('name')}」 출력 값이 수가 아님")
|
||||
cost["계"] = sum(cost[k] for k in COSTS)
|
||||
filled = copy.deepcopy(lg) # 받는 칸에 값만 되돌려 넣음 — 수식 · 참조는 셈한 값으로
|
||||
for b in filled.get("boxes") or []:
|
||||
if b.get("kind") != "function" or not b.get("func"):
|
||||
continue
|
||||
cells = b["func"].setdefault("sheet", {}).setdefault("칸", {})
|
||||
given = values.get(b["id"]) or {}
|
||||
for v in b["func"].get("variables") or []:
|
||||
if v.get("cell"):
|
||||
_place(cells, v["cell"], given.get(v.get("name"), v.get("example")))
|
||||
for box_id, cell, block, value in fills:
|
||||
if box_id == b["id"]:
|
||||
_place(cells, cell, value if block else read(box_id, cell))
|
||||
return {"ports": ports, "outputs": outputs, "cost": cost, "errors": errors, "filled": filled}
|
||||
@@ -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]
|
||||
|
||||
@@ -335,16 +335,17 @@ def materials(q: str, sub: str, detail: str, page: int, size: int) -> dict:
|
||||
return {"total": len(hits), "page": page, "size": size, "items": [_brief_row(r) for r in part]}
|
||||
|
||||
|
||||
RULES = ("규격", "이름", "같음") # 규격에 값 포함 · 이름에 값 포함 · 규격 또는 이름이 값과 같음
|
||||
FIELDS_RULE, OPS_RULE = ("규격", "이름"), ("포함", "같음") # Model CfMaterialRule
|
||||
|
||||
|
||||
def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
"""재료 자리 단가 — 고정 = 품목 · 선택 = 고른 품목(없으면 기본) · 조건 = 범위 + 규칙으로 찾기.
|
||||
"""재료 자리 단가 — 고정 = 품목 · 선택 = 고른 품목(없으면 기본) · 조건 찾기 = 범위 + 규칙.
|
||||
|
||||
slot = {mode: fixed | choice | cond, ref?, default?, scope?: {구분?, 상세구분?, q?}, rule?}
|
||||
조건 찾기: 하나면 그 단가 · 0 이면 「맞는 재료 없음」 · 여럿이면 몇 개인지 + 후보 다섯.
|
||||
slot = Model CfElementRef 꼴 {mode: fixed | choice | rule, ref?, default?,
|
||||
scope?: {구분?, 상세구분?, 찾는말?}, field?: 규격 | 이름, op?: 포함 | 같음}
|
||||
조건 찾기(value = 이어진 알약 값): 하나면 그 단가 · 0 이면 맞는 재료 없음 · 여럿이면 후보 다섯.
|
||||
"""
|
||||
mode = slot.get("mode")
|
||||
mode = slot.get("mode") or "fixed"
|
||||
if mode == "fixed":
|
||||
return _price("재료", str(slot.get("ref") or ""))
|
||||
if mode == "choice":
|
||||
@@ -352,28 +353,27 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
if not ref:
|
||||
return {"ok": False, "reason": "고른 품목도 기본 품목도 없음 — 품목을 골라 주세요"}
|
||||
return _price("재료", str(ref))
|
||||
if mode != "cond":
|
||||
if mode != "rule":
|
||||
return {"ok": False, "reason": f"모르는 재료 방식 「{mode}」"}
|
||||
rule, scope = slot.get("rule") or "규격", slot.get("scope") or {}
|
||||
if rule not in RULES:
|
||||
return {"ok": False, "reason": f"모르는 찾기 규칙 「{rule}」"}
|
||||
field, op, scope = (
|
||||
slot.get("field") or "규격",
|
||||
slot.get("op") or "포함",
|
||||
slot.get("scope") or {},
|
||||
)
|
||||
if field not in FIELDS_RULE or op not in OPS_RULE:
|
||||
return {"ok": False, "reason": f"모르는 찾기 규칙 「{field} {op}」"}
|
||||
if value in (None, ""):
|
||||
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
|
||||
want = _squash(value)
|
||||
rows = _found(
|
||||
str(scope.get("구분") or ""), str(scope.get("상세구분") or ""), scope.get("q") or ""
|
||||
)
|
||||
fits = {
|
||||
"규격": lambda r: want in _squash(r.get("규격")),
|
||||
"이름": lambda r: want in _squash(r.get("이름")),
|
||||
"같음": lambda r: want in (_squash(r.get("규격")), _squash(r.get("이름"))),
|
||||
}[rule]
|
||||
hits = [r for r in rows if fits(r)]
|
||||
sub, detail = str(scope.get("구분") or ""), str(scope.get("상세구분") or "")
|
||||
rows = _found(sub, detail, scope.get("찾는말") or "")
|
||||
if op == "같음":
|
||||
hits = [r for r in rows if _squash(r.get(field)) == want]
|
||||
else:
|
||||
hits = [r for r in rows if want in _squash(r.get(field))]
|
||||
if not hits:
|
||||
where = (
|
||||
" · ".join(str(x) for x in (scope.get("구분"), scope.get("상세구분")) if x) or "전체"
|
||||
)
|
||||
return {"ok": False, "reason": f"맞는 재료 없음 — {where} 에서 {rule} 「{value}」"}
|
||||
where = " · ".join(x for x in (sub, detail) if x) or "전체"
|
||||
return {"ok": False, "reason": f"맞는 재료 없음 — {where} 에서 {field}에 「{value}」 {op}"}
|
||||
if len(hits) > 1:
|
||||
names = ", ".join(f"{r.get('이름')} {r.get('규격') or ''}".strip() for r in hits[:5])
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user