feat(M01): 흐름 로직 입력 약속 하나 — 저장 때 「입력」 은 흐름의 설계 때 정할 것으로 다시 씀 · 지우는 것은 계산 칸만 · 시험판 길 지움

- 흐름으로 저장 = 계산 칸(중간 · 호표 · 결과 · 덧줄)만 지우고 「입력」(이름 · 설명 · 단위 · 고르기 · 범위)은 흐름에서 같은 꼴로 · 조합 · 구조물 · 미리 보기 · 다시 적용은 「입력」 을 그대로 읽음(따로 갈래 없음)
- 엔진 갈래: 안 준 입력 = 멈춤 · 빈 값으로 준 입력 · 단가 없는 품목 · 표에 없는 조건 = 그 값만 비움(알림) · 결과 꼴 = 돈 출력 줄 · 값들 · 알림
- 프로젝트에서 고른 자재 = 흐름 재료 자리 이름으로(고름 · 다시 적용 때 살림)
- 시험판 길 지움: /list · /get · /save · /delete · /user-elements(서버) · cfList · cfGet · cfSave · cfDelete · cfUserElement*(Api) · 빈 _data 폴더
- 시험: 조합에 담은 로직을 흐름으로 바꿔도 입력 목록 · 로직별 결과 · 비목 합 · 단가 묶음 셈이 같음

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 09:57:30 +09:00
co-authored by Claude Opus 5.5
parent 7418571b63
commit 3336902c17
13 changed files with 274 additions and 455 deletions
+31 -7
View File
@@ -185,7 +185,11 @@ def run(flow: dict, key: str, inputs: dict, strict: bool = False) -> dict:
given = {k: _value(v, words) for k, v in (inputs or {}).items()}
flow = {**flow, "key": key}
values = dc._design_values(flow, given)
blank = [d for d in dc.design_inputs(flow) if strict and given.get(d["port"]) is None]
designs = dc.design_inputs(flow) if strict else []
lack = [d["port"] for d in designs if d["port"] not in given]
if lack: # 안 준 입력 — 엔진 `_inputs` 와 같이 멈춤(빈 값으로 준 입력은 아래 「빈 입력」)
raise store.cm.mf.FormulaError(f"「{key}」 입력 「{lack[0]}」 없음")
blank = [d for d in designs if 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])
@@ -200,14 +204,22 @@ def _engine(master, ref: str, row: dict, given: dict) -> dict:
단가 · 값 · 표 = 부른 쪽이 넘긴 master(프로젝트 사본이면 그 단가 · `M02_DF_Values.SOURCE`).
"""
picks = (
store.cm.mf.mp.CHOSEN.get() or {}
) # 고른 자재 — 흐름은 재료 자리 이름으로(호표 차례 아님)
slots = {d["port"] for d in dc.design_inputs(row[FLOW]) if d.get("kind") == "재료 자리"}
given = {**given, **{k: v for k, v in picks.items() if k in slots}}
token = dv.SOURCE.set(master)
try:
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, notes = got["cost"], got.get("notes") or []
# 빈 값(빈 입력 · 단가 없는 품목 · 표에 없는 조건 → #N/A)을 쓴 칸은 엔진처럼 그 값만 비움 — 알림
empty = [e for e in got["errors"] if "#N/A" in e or "단가 없는 품목" in e]
stop = [e for e in got["errors"] if e not in empty]
if stop:
raise store.cm.mf.FormulaError(f"「{ref}」 흐름 셈 — {' · '.join(stop)}")
cost, notes = got["cost"], [*(got.get("notes") or []), *empty]
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):
@@ -216,6 +228,7 @@ def _engine(master, ref: str, row: dict, given: dict) -> dict:
lines = [] # 옛 「줄」 꼴 — 돈 출력 상자마다 한 줄(수량 1 · 단가 = 금액)
for b in outs:
kind, value = (b.get("output") or {}).get("cost"), got["outputs"].get(b["id"])
value = None if value == dc.NA_VALUE else value
if kind in dc.COSTS:
unit = (b.get("output") or {}).get("unit") or ""
lines.append({"이름": b.get("name") or "", "단위": unit,
@@ -346,10 +359,20 @@ def _redo(row: dict, key: str) -> list[str]:
def _bare(row: dict) -> dict:
"""흐름 로직 줄 — 옛 식 칸(입력 · 중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본)
"""흐름 로직 줄 — 계산 칸(중간 · 호표 · 결과 · 덧줄)은 지움(흐름이 정본) · 「입력」 은 흐름의
「설계 때 정할 것」 으로 같은 꼴로 다시 씀(로직 입력 약속 하나 · 설명은 이름이 같으면 살림)
· 끝수 · 시험입력 · 메타 칸은 둠 · 출처 없으면 「자체」."""
kept = {k: v for k, v in row.items() if k not in store.cm.mf.OLD_FIELDS}
return {"끝수": None, **kept, "출처": row.get("출처") or "자체"}
kept = {k: v for k, v in row.items() if k not in store.cm.mf.CALC_FIELDS}
said = {s.get("이름"): s.get("설명") for s in row.get("입력") or [] if s.get("설명")}
inputs = []
for spec in store.cm.mf.inputs_of(row):
one = {
"이름": spec["이름"],
**({"설명": said[spec["이름"]]} if spec["이름"] in said else {}),
}
one |= {k: spec[k] for k in ("단위", "고르기", "범위") if spec.get(k) is not None}
inputs.append(one)
return {"끝수": None, **kept, "입력": inputs, "출처": row.get("출처") or "자체"}
def save(batch: list[dict]) -> dict:
@@ -360,6 +383,7 @@ def save(batch: list[dict]) -> dict:
row = ch.get("row")
if not isinstance(row, dict) or FLOW not in row:
continue
check(row[FLOW]) # 꼴부터 — 입력 약속을 흐름에서 다시 쓰기 전에
new = ch.get("op") == "add" or (ch.get("op") == "edit" and not ch.get("key"))
if new:
if part.get("file") != make.OWN: