fix(m02-df): 흐름 셈 = 옛 셈 — 빈 단가 줄을 부르는 덧줄은 빈 값 · 전수 1,472 모두 맞음

- 빈 단가(blank) 요소 값 = 「#N/A」(서버 Compute · 화면 값 받기 Entry_Detail · Api CfAnswer.blank) → 그 값을 쓰는 식은 빈 값 · 결과가 오류일 때만 끊긴 곳 「단가 없는 품목을 씀」(Eval NoPrice · Compute 같음)
- 변환: 비목 합 · 덧줄의 비목 이름 = 빈 줄 0(SUM(IFERROR…)) · 호표 줄 = IF(수량=0,0,수량×단가) · 줄.이름은 빈 줄이면 빈 값(옛 env 줄)
- 오류 값 칸은 NA() 로(Compute _is_err · Eval isErrorValue)
- Compute 700줄 넘어 표 찾기 · 값 같음을 M02_DF_Lookup.py 로 나눔(Compute 가 다시 내보냄)
- 전수 대조 시험입력 · 기본 입력 1,472/1,472 · 시험 test_m02_df_model 37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
2026-10-02 08:02:10 +09:00
co-authored by Claude Opus 5.5
parent d202a3a6d8
commit d822a08a5e
7 changed files with 231 additions and 101 deletions
+66
View File
@@ -968,3 +968,69 @@ def test_표_찾기_글은_대소문자까지_같아야() -> None:
)
assert got == [[3, None], [1, None], [0.45, None], [0.96, None], [None, "표에 없는 조건"]]
assert [list(cp._lookup(rule, block, {"g": g})) for g in gs] == got
def _fn(i: str, ins: list[str], formula: str) -> dict:
cells = {f"B{k + 2}": {"값": 0} for k in range(len(ins))}
out = f"B{len(ins) + 2}"
cells[out] = {"식": formula}
func = {"sheet": {"id": f"s{i}", "이름": i, "칸": cells}, "variables": []}
func["inputs"] = [{"port": p, "cell": f"B{k + 2}"} for k, p in enumerate(ins)]
func["output"] = {"cell": out, "name": i, "unit": "-"}
return {
"id": i,
"kind": "function",
"name": i,
"color": "#000",
"unit": "-",
"x": 0,
"y": 0,
"func": func,
}
def test_빈_단가_쓰는_식은_빈_값_비목_합은_0_끊긴_곳(monkeypatch) -> None:
g = {"mode": "group", "group": "재료", "variables": [], "elements": [
{"ref": "MT1", "name": "p1", "spec": "", "unit": "-"}, {"ref": "MT2", "name": "p2", "spec": "", "unit": "-"}]} # fmt: skip
boxes = [
{"id": "g", "kind": "input", "name": "재료", "color": "#000", "unit": "-", "x": 0, "y": 0, "input": g},
_fn("a", ["p1"], "IF(2=0,0,2*B2)"),
_fn("b", ["p2"], "IF(1=0,0,1*B2)"),
_fn("z", ["a", "b"], "(B2+B3)*2/100"), # 줄 이름을 부르는 덧줄 — 빈 줄이 끼면 빈 값
_fn("s", ["a", "b", "z"], "SUM(IFERROR(B2,0),IFERROR(B3,0),IFERROR(B4,0))"),
{"id": "o", "kind": "output", "name": "재료비", "color": "#000", "unit": "원", "x": 0, "y": 0,
"output": {"cost": "재료비", "unit": "원"}},
] # fmt: skip
pairs = [
("g", "p1", "a", "p1"),
("g", "p2", "b", "p2"),
("a", "a", "z", "a"),
("b", "b", "z", "b"),
]
pairs += [
("a", "a", "s", "a"),
("b", "b", "s", "b"),
("z", "z", "s", "z"),
("s", "s", "o", "값"),
]
links = [{"id": f"l{k}", "from": {"box": a, "name": ap}, "to": {"box": b, "name": bp}}
for k, (a, ap, b, bp) in enumerate(pairs)] # fmt: skip
lg = {
"key": None,
"name": "t",
"resultUnit": "",
"boxes": boxes,
"links": links,
"userElements": [],
}
values = {"g": {"p1": "#N/A", "p2": 100}}
got = _run(
f"const ev = E.evalLogic({json.dumps(lg, ensure_ascii=False)}, {json.dumps(values)});\n"
"out([ev.cost.재료비, ev.ports.z.z, ev.breaks.map(b => [b.kind, b.box])]);"
)
assert got == [100, "#N/A", [["단가 없는 품목", "a"]]]
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
srv = cp.run(lg, values, lambda key: {}, [])
assert float(srv["cost"]["재료비"]) == 100 and srv["ports"]["z"]["z"] == "#N/A"
assert [[b["kind"], b["box"]] for b in srv["breaks"]] == got[2]