feat(M01): 흐름 로직 줄 = 옛 식 칸(입력 · 중간 · 호표 · 결과 · 덧줄) 지움 · 읽는 곳은 흐름의 설계 때 정할 것 · 출력에서(사용자 결정)

- master_formula.inputs_of · FLOW_INPUTS · OLD_FIELDS · mf.run 이 흐름 줄이면 스스로 서버 갈래를 깖(스크립트 · 시험도)
- check_master 틀 검사 · 시험입력 · 로직(…) 입력 검사 · master_auto 자동값 · 프로젝트 빈 입력 · 연결 · 다시 적용 값이 흐름 입력으로
- 프로젝트 단가 묶음(collect)이 흐름의 요소 · 표 · 다른 로직 · 기계 시간당 경비 로직을 모음
- 옛 엔진 갈래 결과 = 옛 꼴(돈 출력 줄 · 값들 · 알림) · 조합 · 프로젝트에서 안 준 입력은 빈 입력 알림(예시값 아님)
- /text 흐름 로직 = 출력 상자 줄 · GET /logic 은 보기용으로 설계 때 정할 것을 입력 꼴로 얹음(파일은 그대로)
- 찾을 값이 빈 조건 찾기는 그 값만 비움(옛 셈처럼)

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-02 08:55:36 +09:00
co-authored by Claude Opus 5.5
parent 8fed757fc1
commit 28da700d47
13 changed files with 275 additions and 33 deletions
+9 -2
View File
@@ -187,7 +187,11 @@ def get_logics(
@router.get("/logic")
def get_logic(key: str) -> dict:
return _call(store.logic, key)
"""흐름 로직은 「입력」 칸 없음 — 보기용 「설계 때 정할 것」 을 입력 꼴로(파일 그대로)."""
got = _call(store.logic, key)
if isinstance(got["logic"].get("흐름"), dict):
got["logic"] = {**got["logic"], "입력": _num(store.cm.mf.inputs_of(got["logic"]))}
return got
@router.get("/logic/auto")
@@ -233,7 +237,10 @@ def post_calc(body: CalcBody) -> dict:
@router.post("/text")
def post_text(body: CalcBody) -> dict:
"""읽는 식 줄 — 몸은 `/calc` 와 같음."""
"""읽는 식 줄 — 몸은 `/calc` 와 같음 · 흐름 로직은 출력 상자 줄."""
got = _call(flow.text, body.key, body.inputs, body.row)
if got is not None:
return got
with store.cm.mf.mp.choosing(body.고름):
return _call(store.text, body.key, body.inputs, body.row, body.file)
+67 -13
View File
@@ -167,8 +167,10 @@ def _value(v: Any, words: set | frozenset = frozenset()) -> Any:
return store._dec(v)
def run(flow: dict, key: str, inputs: dict) -> dict:
"""흐름 셈 — 입력 = 「설계 때 정할 것」 이름 → 값."""
def run(flow: dict, key: str, inputs: dict, strict: bool = False) -> dict:
"""흐름 셈 — 입력 = 「설계 때 정할 것」 이름 → 값 · strict(조합 · 프로젝트) = 안 준 입력은
예시값
대신 빈 값 + 알림 「빈 입력」(옛 엔진처럼 그 값을 쓰는 곳만 비움)."""
active = _ACTIVE.get()
if key and key in active: # 옛 셈을 건너 다시 돌아온 흐름(흐름 → 옛 로직 → 흐름)
raise store.cm.mf.FormulaError(f"로직이 서로를 부름 — {' → '.join([*active, key])}")
@@ -183,7 +185,12 @@ def run(flow: dict, key: str, inputs: dict) -> dict:
given = {k: _value(v, words) for k, v in (inputs or {}).items()}
flow = {**flow, "key": key}
values = dc._design_values(flow, given)
return dc.run(flow, values, load, flow.get("userElements") or [], [key])
blank = [d for d in dc.design_inputs(flow) if strict and given.get(d["port"]) is None]
for d in blank:
values.setdefault(d["box"], {})[d["port"]] = None
got = dc.run(flow, values, load, flow.get("userElements") or [], [key])
got["notes"] = [*(f"입력 「{d['port']}」 빈 입력" for d in blank), *got.get("notes", [])]
return got
finally:
_ACTIVE.reset(token)
@@ -195,20 +202,65 @@ def _engine(master, ref: str, row: dict, given: dict) -> dict:
"""
token = dv.SOURCE.set(master)
try:
got = run(row[FLOW], ref, given)
got = run(row[FLOW], ref, given, strict=True)
finally:
dv.SOURCE.reset(token)
if got["errors"]:
raise store.cm.mf.FormulaError(f"「{ref}」 흐름 셈 — {' · '.join(got['errors'])}")
cost = got["cost"]
kinds = {(b.get("output") or {}).get("cost") for b in row[FLOW].get("boxes") or []
if b.get("kind") == "output"} # fmt: skip
if kinds and kinds <= {"값"} and len(cost["값들"]) == 1: # 돈 아닌 로직 = 결과 하나
return {"결과": next(iter(cost["값들"].values())), "중간": {}}
return {"줄": [], **{k: cost[k] for k in dc.COSTS}, "계": cost["계"], "중간": {}}
cost, notes = got["cost"], got.get("notes") or []
outs = [b for b in row[FLOW].get("boxes") or [] if b.get("kind") == "output"]
extra = {"중간": {}, "값들": cost["값들"], **({"알림": notes} if notes else {})}
if outs and all((b.get("output") or {}).get("cost") in (None, "값") for b in outs):
first = got["outputs"].get(outs[0]["id"]) # 돈 아닌 로직 = 첫 출력이 결과 · 나머지는 값들
return {"결과": first, **extra}
lines = [] # 옛 「줄」 꼴 — 돈 출력 상자마다 한 줄(수량 1 · 단가 = 금액)
for b in outs:
kind, value = (b.get("output") or {}).get("cost"), got["outputs"].get(b["id"])
if kind in dc.COSTS:
unit = (b.get("output") or {}).get("unit") or ""
lines.append({"이름": b.get("name") or "", "단위": unit,
"수량": Decimal(1), "단가": value, "금액": value, "비목": {kind: value},
**({} if value is not None else {"까닭": "값 없음"})}) # fmt: skip
sums = {k: cost[k] for k in dc.COSTS}
return {"줄": lines, **sums, "계": cost["계"], **extra}
def _inputs(row: dict) -> list[dict]:
"""흐름 로직 입력 = 「설계 때 정할 것」 → 옛 입력 꼴(이름 · 단위 · 고르기 · 범위 · 예시)."""
out = []
for d in dc.design_inputs(row[FLOW]):
v = d.get("variable") or {}
one = {"이름": d["port"], "단위": None if d.get("unit") in (None, "-") else d["unit"]}
one |= {"고르기": v["choices"]} if v.get("choices") else {}
one |= {"범위": v["range"]} if v.get("range") else {}
out.append(one | {"예시": v.get("example")})
return out
store.cm.mf.FLOW_RUN = _engine # 옛 엔진을 부르는 곳이 모두 흐름 로직을 흐름 셈으로(한 곳 갈래)
store.cm.mf.FLOW_INPUTS = (
_inputs # 입력을 읽는 곳(시험입력 · 자동값 · 프로젝트 입력 · 연결)도 흐름에서
)
def text(key: str, inputs: dict, row: dict | None = None) -> dict | None:
"""읽는 식 줄(`/text` 꼴) — 흐름 로직이면 출력 상자를 비목 묶음 줄로 · 아니면 None(옛 글)."""
got = calc(key, inputs, row)
if got is None or not got.get("flow") or "outputs" not in got:
return got
groups: dict[str, list] = {}
for o in got["outputs"]:
value = o["value"]
shown = f"{value:,}" if isinstance(value, (int, Decimal)) else str(value)
line = {"이름": o["name"], "글": shown, "이름글": o["name"], "금액": value, "조각": []}
groups.setdefault(o["cost"] or "값", []).append(line)
out = [{"비목": k, "줄": v} for k, v in groups.items()]
return {
"ok": got["ok"],
"flow": True,
"groups": out,
**({"reason": got["reason"]} if "reason" in got else {}),
}
def auto(key: str) -> dict | None:
@@ -294,9 +346,10 @@ def _redo(row: dict, key: str) -> list[str]:
def _bare(row: dict) -> dict:
"""새 흐름 로직 — 옛 식 칸은 빈 것으로(파일 틀 검사) · 출처 없으면 「자체」."""
old = {"입력": [], "중간": [], "호표": [], "끝수": None}
return {**old, **row, "출처": row.get("출처") or "자체"}
"""흐름 로직 줄 — 옛 식 칸(입력 · 중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본)
· 끝수 · 시험입력 · 메타 칸은 둠 · 출처 없으면 「자체」."""
kept = {k: v for k, v in row.items() if k not in store.cm.mf.OLD_FIELDS}
return {"끝수": None, **kept, "출처": row.get("출처") or "자체"}
def save(batch: list[dict]) -> dict:
@@ -313,6 +366,7 @@ def save(batch: list[dict]) -> dict:
raise store.StoreError(400, f"새 흐름 로직은 「{make.OWN}」 에만 더함")
row = make._clean(_bare(row), str(row.get("소유") or make.OWNERS[0]))
ch.update(op="add", key=None)
row = _bare(row) if not new else row
ch["row"] = row
name = row.get("이름") or ch.get("key") or "새 로직"
warns += [f"「{name}」 {w}" for w in _redo(row, str(ch.get("key") or ""))]
@@ -146,7 +146,7 @@ def _element_value(
slot = {"field": rule.get("field"), "op": rule.get("op"), "scope": el.get("scope"),
"ref": el.get("ref"), "unit": el.get("unit")} # fmt: skip
got = dv.group_find(group, slot, ports.get(src.get("box"), {}).get(src.get("name")))
if not got["ok"] and el.get("default"):
if not got["ok"] and not got.get("blank") and el.get("default"):
errors.append(f"「{box.get('name')} · {el.get('name')}」 {got['reason']} — 기본으로 셈")
got = dv.one_value({"group": group, "ref": el["default"], "unit": el.get("unit")})
elif given is not None:
+15 -3
View File
@@ -431,7 +431,11 @@ def _filled(template: str, value: Any) -> str:
def _search(slot: dict, value: Any) -> dict:
"""조건 찾기 field 검색어 — 틀(요소 ref 묶음)의 {자리} 를 알약 값으로 채워 옛 찾기 그대로."""
if value in (None, ""):
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
return {
"ok": False,
"blank": True,
"reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요",
}
cond = _pick(slot.get("ref")) # 요소 ref = 옛 검색어 묶음(규격 · 지역 칸도 그대로)
if cond is None: # 묶음이 없으면 범위(구분 · 상세구분) + 채운 검색어
scope = slot.get("scope") or {}
@@ -468,7 +472,11 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
if field == "검색어":
return _search(slot, value)
if value in (None, ""):
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
return {
"ok": False,
"blank": True,
"reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요",
}
want = _squash(value)
sub, detail = str(scope.get("구분") or ""), str(scope.get("상세구분") or "")
rows = _found(sub, detail, scope.get("찾는말") or "")
@@ -520,7 +528,11 @@ def group_find(group: str, slot: dict, value: Any) -> dict:
· field 가 없으면 키(원문번호) 또는 이름이 같은 줄 · 하나면 그 값(`one_value`).
"""
if value in (None, ""):
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
return {
"ok": False,
"blank": True,
"reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요",
}
field, op, want = slot.get("field"), slot.get("op") or "같음", _squash(value)
words = str((slot.get("scope") or {}).get("찾는말") or "").split()
+1 -1
View File
@@ -211,7 +211,7 @@ def write_file(
def _kept_values(values: Any, labor: dict[str, Any]) -> dict[str, Any]:
"""같은 로직 · 같은 입력 이름만 살림."""
names = {
str(row.get("키")): {spec["이름"] for spec in row.get("입력", [])}
str(row.get("키")): {spec["이름"] for spec in prices.mf.inputs_of(row)}
for row in labor.get("로직", [])
}
out: dict[str, Any] = {}
+48 -1
View File
@@ -125,6 +125,50 @@ class _Collector:
for key in mf.candidates(self.whole, one, None, None):
self.add(key)
def flow(self, row: dict) -> None:
"""흐름 입력 상자 — 요소(고정 · 기본 · 검색어 묶음 · 조건 찾기 후보) · 표 · 다른 로직 ·
기계 시간당 경비 로직."""
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv # 흐름 값 찾기와 같은 뜻
for box in row["흐름"].get("boxes") or []:
inp = (box.get("input") or {}) if box.get("kind") == "input" else {}
if inp.get("mode") == "logic":
self.logic(str((inp.get("logic") or {}).get("key") or ""))
if inp.get("table"):
self.add(str(inp["table"].get("key") or ""))
for el in inp.get("elements") or []:
ref, unit = str(el.get("ref") or ""), el.get("unit")
for one in (ref, str(el.get("default") or "")):
cond = dv._pick(one)
if cond is not None:
self.pick(row, cond, unit)
elif one and not one.startswith("UE-"):
self.element(one)
if el.get("mode") != "rule":
continue
if inp.get("group") in dv.GROUP_RULE: # 인력 · 기계 · 유가전력 찾기 = 그 그룹 전부
self.keys.update(str(r.get("키")) for r in dv._group_rows(inp["group"]))
elif dv._pick(ref) is None: # 재료 범위 찾기 — 범위(구분 · 상세구분) 후보
scope = el.get("scope") or {}
self.pick(
row, {k: scope[k] for k in ("구분", "상세구분") if scope.get(k)}, unit
)
def element(self, ref: str) -> None:
"""요소 키 · 「EQ키#칸」(시간당 경비면 그 기계경비 로직까지) · 「ID:원문번호{…}」 틀."""
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
base, _, field = ref.partition(dv.MACHINE_SEP)
if "{" in base: # 「EQ:{기계}」 같은 틀 — 그 테이블 전부
self.keys.update(self.whole.index.get(mk.id_of(base) or "", {}))
return
self.add(base)
if field == dv.HOURLY:
code = str((self.whole.index.get(base[:2], {}).get(base) or {}).get("원문번호") or "")
for fit in dv._machine_family():
if code in [str(c) for c in dv._codes(fit)]:
self.logic(str(fit.get("키")))
def logic(self, key: str) -> None:
if key in self.logics:
return
@@ -135,6 +179,9 @@ class _Collector:
return
self.logics.add(key)
self.keys.add(key)
if isinstance(row.get("흐름"), dict): # 흐름 로직 — 옛 칸 대신 흐름이 쓰는 것을 모음
self.flow(row)
return
for text in _texts(row):
try:
node = mf.parse(text)
@@ -201,7 +248,7 @@ def missing_inputs(snapshot: Any, labor: dict[str, Any], values: dict[str, Any])
except mf.FormulaError:
continue
mine = values.get(key) or {}
for spec in row.get("입력", []):
for spec in mf.inputs_of(row):
got = mine.get(spec["이름"])
if not isinstance(got, dict) or got.get("값") in (None, ""):
out.append({"로직": key, "입력": spec["이름"]})
@@ -84,7 +84,7 @@ def inputs(key: str) -> dict[str, Any]:
for logic in logics:
auto = m01.mau.values(whole, logic)
specs = []
for spec in logic.get("입력") or []:
for spec in m01.cm.mf.inputs_of(logic):
one = {k: spec[k] for k in _SPEC if k in spec}
one.setdefault("설명", None)
one.setdefault("단위", None)
@@ -126,7 +126,9 @@ def write_file(column: str, doc: dict[str, Any]) -> None:
def _known(key: str) -> dict[str, set[str]]:
"""{로직 키: {입력 이름}} — 그 항목 조합이 지금 담은 것."""
_, logics, _ = combo_logics(_item(key).get("일위대가"))
return {str(lg.get("키")): {str(s.get("이름")) for s in lg.get("입력") or []} for lg in logics}
return {
str(lg.get("키")): {str(s.get("이름")) for s in m01.cm.mf.inputs_of(lg)} for lg in logics
}
def read_links(key: str) -> dict[str, Any]:
@@ -204,7 +206,7 @@ def pills(key: str, place: str) -> dict[str, Any]:
if row.get("자리") != place or row.get("사라짐"):
continue
logic = by_key[str(row["로직"])]
spec = next(s for s in logic.get("입력") or [] if s.get("이름") == row["입력"])
spec = next(s for s in m01.cm.mf.inputs_of(logic) if s.get("이름") == row["입력"])
short = combo_store._TAIL.sub("", str(logic.get("이름") or "")) or row["로직"]
linked.append(
{
@@ -295,8 +295,9 @@ def check_form(name: str, data: dict) -> list[str]:
f" 이 파일 머리·이름 「{want[0]} · {want[1]}」 과 다름"
)
cols = COLS.get(book)
flow = isinstance(row.get("흐름"), dict) # 흐름 로직 — 옛 식 칸 없음
for need in cols or ROW_KEYS[kind]:
if need not in row:
if need not in row and not (flow and need in mf.OLD_FIELDS):
out.append(f"{where} · 칸 「{need}」 없음")
if "출처" in row and not _source_ok(row["출처"]):
out.append(f"{where} · 출처 모양 「{row['출처']}」")
@@ -312,7 +313,7 @@ def check_form(name: str, data: dict) -> list[str]:
out.append(f"{where} · 「{bundle}」 묶음 칸이 값 묶음과 다름")
if kind == "표":
out += _check_table(where, row)
if kind == "로직" and "결과" not in row and "호표" not in row:
if kind == "로직" and "결과" not in row and "호표" not in row and not flow:
out.append(f"{where} · 「호표」 도 「결과」 도 없음")
return out
@@ -497,7 +498,7 @@ def 시험입력(row: dict) -> dict:
"""일괄 시험 계산용 입력 한 벌 — 고르기 첫 값 · 범위 위끝 · 줄의 「시험입력」 이 이김.
시험 계산 전용 — 정본 계산은 화면이 준 값만 씀."""
given = {}
for spec in row.get("입력", []):
for spec in mf.inputs_of(row):
if "고르기" in spec:
given[spec["이름"]] = spec["고르기"][0]
elif "범위" in spec:
+3 -1
View File
@@ -78,7 +78,9 @@ def _lowest(master: mf.Master, uses: list):
def values(master: mf.Master, row: dict) -> dict:
"""로직 줄 하나의 입력마다 자동값."""
"""로직 줄 하나의 입력마다 자동값 · 흐름 로직은 「설계 때 정할 것」 예시값."""
if isinstance(row.get("흐름"), dict):
return {s["이름"]: s.get("예시") for s in mf.inputs_of(row)}
finds, divs = _uses(row)
out = {}
for spec in row.get("입력", []):
@@ -7,9 +7,12 @@
from __future__ import annotations
import importlib
import itertools
import re
import sys
from decimal import ROUND_DOWN, ROUND_HALF_UP, ROUND_UP, Decimal
from pathlib import Path
import master_keys as mk
@@ -31,6 +34,26 @@ _ARITH = re.compile(r"[\d.\s+\-*/()]*\d[\d.\s+\-*/()]*") # 분수·식 값 칸(
# 흐름도 로직(줄에 「흐름」) 셈 — 서버(`M01_MasterData_Store_Flow`)가 꽂음 · 없으면 옛 셈만
# 꼴 = (master, 키, 줄, 입력) → `run` 과 같은 꼴(돈 = 줄 · 비목 합 · 계 / 돈 아님 = 결과)
FLOW_RUN = None
# 흐름 로직 입력 = 「설계 때 정할 것」 → 입력 꼴(이름 · 단위 · 고르기 · 범위 · 예시) — 서버가 꽂음
FLOW_INPUTS = None
OLD_FIELDS = ("입력", "중간", "호표", "결과", "덧줄") # 흐름 로직 줄에는 없음(흐름 하나가 정본)
def _flow_hooks() -> None:
"""흐름 셈 · 입력 갈래를 깖 — 서버가 안 깔았으면(스크립트 · 시험) 여기서 서버 모듈을 불러 깖."""
if FLOW_RUN is None or FLOW_INPUTS is None:
root = str(Path(__file__).resolve().parents[3])
if root not in sys.path:
sys.path.insert(0, root)
importlib.import_module("M01_MasterData.M01_MasterData_Store_Flow")
def inputs_of(row: dict) -> list[dict]:
"""로직 입력 — 흐름 로직이면 흐름의 「설계 때 정할 것」 · 아니면 줄의 「입력」."""
if isinstance(row.get("흐름"), dict):
_flow_hooks()
return FLOW_INPUTS(row)
return row.get("입력", [])
class FormulaError(ValueError):
@@ -485,7 +508,7 @@ def fill_empty(row: dict, given: dict) -> dict:
"""화면이 안 보낸 가격 입력(단위에 「원」)은 null 로 채움 — 빈 입력과 같이 그 줄만 비움."""
gap = {
s["이름"]: None
for s in row.get("입력", [])
for s in inputs_of(row)
if s["이름"] not in given and "원" in str(s.get("단위") or "")
}
return {**given, **gap}
@@ -543,7 +566,8 @@ def _blank_line(item: dict, why: str) -> dict:
def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
"""로직 한 줄 계산 — 돈 로직은 호표 줄 · 비목 합 · 계, 돈 아닌 로직은 결과 수 하나."""
row = master.logic(ref)
if FLOW_RUN is not None and isinstance(row.get("흐름"), dict):
if isinstance(row.get("흐름"), dict): # 흐름 로직 — 깔림에 기대지 않고 스스로 깖
_flow_hooks()
return FLOW_RUN(master, ref, row, given)
env = _inputs(row, given)
for step in row.get("중간", []):
@@ -641,7 +665,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
# ── 검사 ──────────────────────────────────────────────────────────────
def _choices(row: dict, name: str, master: Master):
"""이름이 가질 수 있는 값 — 고르기 입력, 또는 고르기 입력만 끼운 요소 칸인 중간. 모르면 None."""
for spec in row.get("입력", []):
for spec in inputs_of(row):
if spec["이름"] == name:
return spec.get("고르기")
for step in row.get("중간", []):
@@ -729,7 +753,7 @@ def _check_node(row, node, known: set, master: Master, where: str, out: list, ca
except FormulaError as e:
out.append(f"{where} · {e}")
continue
names = {s["이름"] for s in target.get("입력", [])}
names = {s["이름"] for s in inputs_of(target)}
given = {k for k, _ in part[2]}
for extra in sorted(given - names):
out.append(f"{where} · 「{master.label(part[1])}」 에 입력 「{extra}」 없음")
@@ -40,7 +40,7 @@ def test_옛_엔진도_흐름_로직은_흐름_셈(client: TestClient) -> None:
_save_flow(client, KEY, _flow())
whole = store.cm.mf.Master(store.cm.load(folder=store.FOLDER))
got = store.cm.mf.run(whole, KEY, {"길이": Decimal(4), "폭": Decimal(5)})
assert got["줄"] == [] and got["노무비"] == 20000 and got["계"] == 20000
assert [(x["이름"], x["금액"]) for x in got["줄"]] == [("노무비", 20000)] and got["계"] == 20000
line = {
"종류": "로직",
"요소": f"로직({KEY}, 길이=4, 폭=5)",
@@ -0,0 +1,65 @@
# ruff: noqa: E501
"""흐름 로직(옛 식 칸 지운 줄)이 조합 · 프로젝트 길에서도 옛 셈과 같음(PLAN 36 · 사용자 결정 · 사본 폴더).
못박는 것 — 옛 로직 하나를 변환해 흐름으로 저장(옛 칸 지움)한 뒤
- 옛 엔진 `mf.run`(조합 · 프로젝트 길) 합계 = 바꾸기 전 옛 셈 합계
- 프로젝트 단가 묶음(`M02_Item_Prices.collect`)만으로 셈 = 같은 합계(흐름이 쓰는 요소 · 표 · 다른 로직이 담김)
- 입력 목록(`mf.inputs_of`) = 옛 입력 이름 · 안 준 입력은 「빈 입력」 알림(예시값으로 안 넘어감)
"""
from __future__ import annotations
import shutil
from decimal import Decimal
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Flow as sflow
from M02_MasterTemplete import M02_Item_Prices as prices
from M02_MasterTemplete.DataFlow import M02_DF_Convert as cv
REAL = store.FOLDER
KEYS = ("GF000185", "GF000219") # 레미콘 검색어 · 펌프차 기계 · 인력 / 표 찾기 · 인력
@pytest.fixture
def folder(tmp_path: Path, monkeypatch) -> Path:
for path in REAL.glob("*.json"):
shutil.copy(path, tmp_path / path.name)
monkeypatch.setattr(store, "FOLDER", tmp_path)
return tmp_path
def _sums(got: dict) -> dict:
return {k: round(Decimal(str(got[k])), 6) for k in ("노무비", "재료비", "경비", "계")}
@pytest.mark.parametrize("key", KEYS)
def test_흐름으로_바꾼_로직_조합_프로젝트_길도_옛_셈과_같음(folder: Path, key: str) -> None:
mf = store.cm.mf
got = store.logic(key)
old_row = got["logic"]
names = [s["이름"] for s in old_row.get("입력") or []]
given = {k: v for k, v in store.cm.시험입력(old_row).items()}
before = _sums(mf.run(mf.Master(store.cm.load(folder=folder)), key, dict(given)))
flow = cv.convert(old_row, {"key": key, "master": store.loaded()[1]})["flow"]
flow.pop("key", None)
part = {
"file": got["file"],
"version": got["version"],
"changes": [{"op": "edit", "key": key, "row": {**old_row, "흐름": flow}}],
}
sflow.save([store._dec(part)])
row = store.logic(key)["logic"]
assert not set(row) & set(mf.OLD_FIELDS)
assert [s["이름"] for s in mf.inputs_of(row)] == names
assert _sums(mf.run(mf.Master(store.cm.load(folder=folder)), key, dict(given))) == before
bundle = prices.collect([key])
assert "_빠진" not in bundle, bundle.get("_빠진")
snapshot = prices.load_prices(prices.to_json(bundle))
assert _sums(mf.run(snapshot, key, dict(given))) == before # 단가 묶음만으로 같은 합계
if key == "GF000185": # 빈 입력 — 재료 찾기 자리(표 찾기 빈 조건은 sub7 lookup 몫)
blank = mf.run(snapshot, key, {k: v for k, v in given.items() if k != names[0]})
assert any(f"「{names[0]}」 빈 입력" in n for n in blank.get("알림") or [])
@@ -168,6 +168,9 @@ def test_신규_흐름_로직은_자체_GX_복사는_흐름도(client: TestClien
assert key.startswith("GX")
saved = _get(client, "/api/m01/logic", key=key)["logic"]
assert saved["구분"] == "자체" and saved["흐름"]["name"] == "시험 흐름 로직"
assert not set(store.logic(key)["logic"]) & set(
store.cm.mf.OLD_FIELDS
) # 파일 줄에 옛 식 칸 없음
assert (
Decimal(str(_post(client, "/api/m01/calc", {"key": key, "inputs": {}})["sums"]["계"]))
== 6000
@@ -252,3 +255,28 @@ def test_저장_안_한_고친_줄_변환(client: TestClient, folder: Path) -> N
if importlib.util.find_spec(sflow.CONVERT) is not None:
assert got["flow"]["name"] == "고친 이름" and got["flow"]["boxes"]
assert _hashes(folder) == before
def test_흐름_로직_줄은_옛_식_칸_없음_읽는_곳은_흐름에서(client: TestClient, folder: Path) -> None:
"""흐름으로 저장 = 입력 · 중간 · 호표 · 결과 · 덧줄 지움(사용자 결정) · 입력을 읽는 곳은 「설계 때 정할 것」."""
mf = store.cm.mf
_save_flow(client, KEY, _flow())
row = store.logic(KEY)["logic"] # 파일 줄 그대로
assert not set(row) & set(mf.OLD_FIELDS) and "끝수" in row and row["흐름"]
view = _get(client, "/api/m01/logic", key=KEY)[
"logic"
] # 화면 보기 = 설계 때 정할 것을 입력 꼴로
assert [x["이름"] for x in view["입력"]] == ["길이", "폭"]
assert [(s["이름"], s["단위"], s["예시"]) for s in mf.inputs_of(row)] == [
("길이", "m", 2),
("폭", "m", 3),
]
assert {"길이", "폭"} <= set(store.cm.시험입력(row)) # 줄의 시험입력 칸은 그대로 둠(이김)
assert store.mau.values(store.loaded()[1], row) == {"길이": 2, "폭": 3}
assert _get(client, "/api/m01/logic/auto", key=KEY) == {"값": {"길이": 2, "폭": 3}}
text = _post(client, "/api/m01/text", {"key": KEY, "inputs": {"길이": 4, "폭": 5}})
assert text["flow"] is True and text["groups"][0]["비목"] == "노무비"
assert Decimal(str(text["groups"][0]["줄"][0]["금액"])) == 20000
whole = mf.Master(store.cm.load(folder=folder)) # 옛 엔진 길(조합 · 프로젝트 단가)
assert mf.run(whole, KEY, {"길이": Decimal(4), "폭": Decimal(5)})["계"] == 20000
assert _get(client, "/api/m01/logic", key=KEY)["blocked"] is False