fix(m02-df): 변환 흐름 화면 흠 넷 — 변수 예시값 · 표 찾기 실패 같은 뜻 · 다른 로직 서버 값 · 합계 끝수

- 변환: 설계 값 변수 예시값 = 줄 시험입력 · 없으면 check_master.시험입력 기본(글 수 → 수) · totalRound = null(옛 계는 끝수 없음)
- Model CfLogic.totalRound(없음 = 기본 계금 1원 미만 버림 · null = 끝수 없음) · Round totalRoundFor · roundBy/roundText 가 null 받음 · Summary · M01 흐름 보기 합계가 그것을 씀
- Compute: 함수 결과 오류는 오류 값(「#N/A」 등)으로 넘김(화면 Eval 과 같음 · 받는 칸으로 번짐 · 옛 엔진은 그 줄을 비움) — 서버가 빈 칸 0 으로 계속 세던 차이 없앰
- 화면 값 물음(Entry_Detail queriesOf): 인력 · 기계 · 유가 조건 찾기도 자리 물음 · 재료 검색어 틀은 채운 글 · 다른 로직 입력은 서버 /logic-value 로 받은 설계 값째 셈(그 안 조건 찾기 · 단가까지) · Eval 은 서버 출력이 있으면 그것
- Values.one_value: 인력 · 기계 · 유가 자리 물음 = group_find
- 전수 대조 시험입력 1,414/1,472(96.1%) · 기본 입력 1,429(97.1%) — 남은 49 는 sub4 범위규칙 칸 · 검색어 찾기 · 재료 값 차이

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 00:56:28 +09:00
co-authored by Claude Opus 5.5
parent a774b4e97b
commit d2ab8ee8cc
14 changed files with 156 additions and 25 deletions
+15 -1
View File
@@ -125,7 +125,8 @@ def _keys() -> list[str]:
REASONS = [ # (알림에 든 말, 까닭) — 앞이 먼저
("표에 여럿 맞음", "표 범위 규칙 다름 — 여럿 맞음(이상·이하 · 이상·미만 경계)"),
("표에 없는 조건", "표 범위 규칙 다름 — 없는 조건(이상·이하 · 이상·미만 경계)"),
("같음", "재료 검색어 자리 — 서버 조건 찾기(키 같음) ≠ 옛 검색어 찾기"),
("검색어", "재료 검색어 틀 — 서버 검색어 찾기 대기(sub4)"),
("같음", "조건 찾기 못 찾음(키 같음)"),
("맞는 재료 없음", "재료 빈 검색어 — 서버 재료 값 찾기 다름"),
("비어 있음", "단가 빈 품목 — 서버 처리 다름"),
("값 없음", "단가 빈 품목 — 서버 처리 다름"),
@@ -270,3 +271,16 @@ def test_전수_대조_옛_셈_같음() -> None:
r for r in rows if r["시험입력"]["state"] == "흐름 셈 멈춤"
][:3]
assert report["시험입력"]["맞는 비율"] >= 0.5, report["시험입력"]
def test_변수_예시값_합계_끝수() -> None:
_copy()
got = _shape("GF000007", "GF000226")
for key, one in got.items():
flow = one["flow"]
design = next(b for b in flow["boxes"] if b["id"] == "in-design")
assert all(v["example"] is not None for v in design["input"]["variables"]), key
assert flow["totalRound"] is None # 옛 계는 끝수 없음
box = next(b for b in got["GF000226"]["flow"]["boxes"] if b["id"] == "in-design")
vars226 = {v["name"]: v["example"] for v in box["input"]["variables"]}
assert vars226["직경"] == 60 # 줄의 시험입력(글 「60.0」 → 수)
+37 -3
View File
@@ -854,9 +854,8 @@ def test_표_찾기_빈_조건칸_무엇이든_빈_값줄_건너뜀_실패는_
monkeypatch.setattr(cp.dv, "one_value", lambda item, local=None: {"ok": True, "rows": BLOCK})
for v, (screen_out, screen_breaks) in zip(vals, got):
srv = cp.run(lg, {"v": v["v"]}, lambda key: {}, [])
out = srv["outputs"]["o"]
want = None if isinstance(screen_out, str) and screen_out.startswith("#") else screen_out
assert (None if out is None else float(out)) == (None if want is None else float(want))
out = srv["outputs"]["o"] # 오류 값은 화면과 같은 글(「#N/A」)
assert out == screen_out if isinstance(screen_out, str) else float(out) == float(screen_out)
assert [b["kind"] for b in srv["breaks"]] == screen_breaks
@@ -915,3 +914,38 @@ def test_표_찾기_범위_경계_규칙_칸() -> None:
)
assert got == [[1, None], [2, None], [3, None], [3, None], [None, "표에 없는 조건"]]
assert [list(cp._lookup(rule, block, {"x": x})) for x in xs] == got
def test_화면_값_물음_조건_찾기_인력기계유가_검색어_틀_다른_로직() -> None:
lg = {
"key": None, "name": "t", "resultUnit": "", "userElements": [],
"boxes": [
{"id": "v", "kind": "input", "name": "설계", "input": {"mode": "variable", "elements": [], "variables": []}},
{"id": "g", "kind": "input", "name": "기계", "input": {"mode": "group", "group": "기계", "variables": [], "elements": [
{"ref": "EQ:{기계}#가격", "name": "가격 자리", "slot": "가격 자리", "spec": "", "unit": "-", "mode": "rule",
"rule": {"field": "키", "op": "같음", "from": {"box": "v", "name": "기계"}}}]}},
{"id": "m", "kind": "input", "name": "재료", "input": {"mode": "group", "group": "재료", "variables": [], "elements": [
{"ref": "{}", "name": "레미콘 자리", "slot": "레미콘 자리", "spec": "", "unit": "㎥", "mode": "rule", "scope": {"구분": "레미콘"},
"rule": {"field": "검색어", "op": "같음", "from": {"box": "v", "name": "a"}, "template": "레미콘 {a}-{b}",
"slots": [{"name": "a", "from": {"box": "v", "name": "a"}}, {"name": "b", "from": {"box": "v", "name": "b"}}]}}]}},
{"id": "o", "kind": "input", "name": "로직", "input": {"mode": "logic", "elements": [], "variables": [], "logic": {
"key": "GC000270", "outputs": [{"name": "노무비", "unit": "원"}], "designs": [{"name": "기계", "unit": "-"}, {"name": "지역", "unit": "-"}],
"examples": {"지역": "전국평균"}}}},
],
"links": [{"id": "l1", "from": {"box": "v", "name": "기계"}, "to": {"box": "o", "name": "기계"}}],
} # fmt: skip
pills = {"기계": "0201-0020", "a": 25, "b": "18"}
got = _run(
"cache.set(path.resolve(path.join(ROOT, 'M01_MasterData', 'M01_MasterData_UI_Logic_Api.ts')), { exports: { fetchLogics: async () => [] } });\n"
"const D = loadTs(path.join(ROOT, 'M02_MasterTemplete', 'DataFlow', 'M02_DF_Entry_Detail.ts'));\n"
f"const lg = {json.dumps(lg, ensure_ascii=False)};\nconst pills = {json.dumps(pills, ensure_ascii=False)};\n"
"const qs = D.queriesOf(lg, (p) => pills[p.name] ?? null, M.elementPort)"
".map(a => [a.port, a.q.group, a.q.value ?? null, a.q.logic ?? null, !!a.q.slot]);\n"
"out([qs, E.evalLogic(lg, { o: { 노무비: 123 } }, { logics: {} }).ports.o]);"
)
qs, server = got
assert ["가격 자리", "기계", "0201-0020", None, True] in qs
assert ["레미콘 자리", "재료", "레미콘 25-18", None, True] in qs
designs = {"기계": "0201-0020", "지역": "전국평균"}
assert ["*", "기계", None, {"key": "GC000270", "designs": designs}, False] in qs
assert server == {"노무비": 123} # 서버가 셈한 다른 로직 출력이 있으면 그것
+2 -1
View File
@@ -573,7 +573,8 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien
assert any("맞는 재료 없음" in e for e in bad["errors"]) and any(
"표에 없는 조건" in e for e in bad["errors"]
)
assert bad["ports"]["f"]["합"] is None and _d(bad["cost"]["재료비"]) == 0
# 함수 결과 오류 = 오류 값(화면 Eval 과 같음 · 받는 칸으로 번짐)
assert bad["ports"]["f"]["합"] == "#N/A" and _d(bad["cost"]["재료비"]) == 0
def test_표준_표_읽기_쓰기_처음값(client: TestClient, folder: Path) -> None: