feat(m02-df): 흐름도 서버 — 재료 자리 셋 · 자재 찾기 · 다른 로직 값 · 설계 때 정할 것 · 표준 표 · Eval 과 같은 차례로 다시 셈

- M02_DF_Compute.py — 화면 Eval(evalLogic)과 같은 차례(Model depends) · 선 규칙은 Rule.ts applyRule 과 같은 뜻(값 기준 · 막히면 받는 상자 멈춤) · 끝수 = ROUND 계열 · 함수 시트는 같은 엔진 Node(서로 안 기대는 것은 한 번에) · 다른 로직은 받은 설계 값으로 셈 · 서로 부르면 막음 · 받는 칸에 값만 되돌림
- M02_DF_Values.py(Router 에서 나눔) — 재료 고정 · 선택(고른 품목 · 기본) · 조건 찾기(범위 + 규격/이름 포함/같음 · 0 · 여럿 후보) · 자재 찾기(띄어쓰기 AND · 이름 · 규격 · 키 · 원문번호 · 쪽) · 표준 표
- Router — GET /materials · /design-inputs · POST /logic-value · GET/PUT/DELETE /standards · main.py 등록
- Api — cfMaterials · cfLogicValue · cfDesignInputs · cfStandards(Save · Reset) · slot 물음
- 시험 — 재료 셋 · AND · 다른 로직 · 순환 · 선 규칙 넷 · 끝수 · 설계 때 정할 것 · 표준 표 · 권한

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-01 19:58:10 +09:00
co-authored by Claude Opus 5.5
parent 3b4ce57d1e
commit 868dc8e37f
3 changed files with 404 additions and 294 deletions
+42 -29
View File
@@ -1,3 +1,4 @@
# ruff: noqa: E501 — 견본 상자 · 선 덩어리는 한눈에 보이게 한 줄로 둠
"""단가산출 다이어그램 시험 길(`M02_MasterTemplete/DataFlow/M02_DF_Router.py`) 시험 — PLAN 35-7.
· Model CfLogic(boxes · links · userElements) 저장 → 열기 → 목록 → 지우기 · 견본(`_sample`)은 읽기만
@@ -482,22 +483,17 @@ def test_다른_로직_값_설계_값_순환(client: TestClient) -> None:
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClient, folder: Path) -> None:
"""Eval 과 같은 뜻 — 규칙은 값 기준 · 막힌 선(통과 안 됨 · 표에 없는 조건)은 받는 상자를 멈춤."""
keys = _add_pipes(folder)
scope = {"구분": "시험구분", "상세구분": "시험상세", "찾는말": "견본관"}
vars_ = [{"name": "돌", "type": "choice", "unit": "-", "example": "견치돌"},
{"name": "높이", "type": "number", "unit": "m", "example": 4},
vars_ = [{"name": "돌", "type": "choice", "unit": "-", "example": "깬돌"},
{"name": "높이", "type": "number", "unit": "m", "example": 2},
{"name": "관경", "type": "text", "unit": "-", "example": "D300"}] # fmt: skip
pipe = {"ref": "", "name": "관", "spec": "", "unit": "m", "mode": "rule", "scope": scope,
pipe = {"ref": "", "name": "관", "slot": "관 자리", "spec": "", "unit": "m", "mode": "rule", "scope": scope,
"rule": {"field": "규격", "op": "포함", "from": {"box": "v", "name": "관경"}}} # fmt: skip
pick = {
"ref": "",
"name": "골라 쓰는 관",
"spec": "",
"unit": "m",
"mode": "choice",
"default": keys[1],
}
sheet = {"id": "s", "이름": "셈", "칸": {"B1": {"식": "A1+A2+A3+A4"}}}
pick = {"ref": "", "name": "관2", "slot": "골라 쓰는 관", "spec": "", "unit": "m", "mode": "choice",
"default": keys[1]} # fmt: skip
sheet = {"id": "s", "이름": "셈", "칸": {"B1": {"식": "A1+A2+A3"}}}
lg = {
"key": None, "name": "규칙 견본", "resultUnit": "원",
"boxes": [
@@ -506,41 +502,58 @@ def test_선_규칙_끝수_재료_조건_설계_때_정할_것(client: TestClien
"variables": [], "table": {"key": SAMPLE}}),
_box("m", "input", "재료", input={"mode": "group", "group": "재료", "elements": [pipe, pick],
"variables": []}),
_box("f", "function", "셈", func={**_func(sheet, [{"port": "품", "cell": "A1"},
{"port": "관", "cell": "A2"}, {"port": "통과", "cell": "A3"}, {"port": "막힘", "cell": "A4"}],
{"cell": "B1", "name": "합", "unit": "원", "round": {"places": 0, "method": "버림"}})}),
_box("o", "output", "재료비", output={"cost": "재료비", "unit": "원", "round": {"places": -1, "method": "버림"}}),
_box("f", "function", "셈", func=_func(sheet, [{"port": "품", "cell": "A1"},
{"port": "관", "cell": "A2"}, {"port": "높이", "cell": "A3"}],
{"cell": "B1", "name": "합", "unit": "원", "round": {"places": 0, "method": "버림"}})),
_box("o", "output", "재료비", output={"cost": "재료비", "unit": "원",
"round": {"places": -1, "method": "버림"}}),
_box("o2", "output", "골라 쓴 관", output={"cost": "값", "unit": "원"}),
_box("o3", "output", "낮을 때만", output={"cost": "값", "unit": "m"}),
],
"links": [
{**_link("l1", "t", "표", "f", "품"), "rule": {"kind": "lookup", "col": "석공", "conds": [
{"col": "돌", "from": {"box": "v", "name": "돌"}}, {"col": "높이", "from": {"box": "v", "name": "높이"}}]}},
{**_link("l2", "m", "관", "f", "관"), "rule": {"kind": "scale", "op": "+%", "value": 10}},
{**_link("l3", "v", "높이", "f", "통과"), "rule": {"kind": "gate", "from": {"box": "v", "name": "높이"}, "op": "≥", "value": 3}},
{**_link("l4", "v", "높이", "f", "막힘"), "rule": {"kind": "gate", "from": {"box": "v", "name": "높이"}, "op": "<", "value": 3}},
_link("l5", "f", "합", "o", "값"),
{**_link("l6", "m", "골라 쓰는 관", "o2", "값"), "rule": {"kind": "round", "places": -3, "method": "반올림"}},
{**_link("l2", "m", "관 자리", "f", "관"), "rule": {"kind": "scale", "op": "+%", "value": 10}},
{**_link("l3", "v", "높이", "f", "높이"), "rule": {"kind": "gate", "from": {"box": "v", "name": "높이"},
"op": "≥", "value": 1}},
_link("l4", "f", "합", "o", "값"),
{**_link("l5", "m", "골라 쓰는 관", "o2", "값"), "rule": {"kind": "round", "places": -3, "method": "반올림"}},
{**_link("l6", "v", "높이", "o3", "값"), "rule": {"kind": "gate", "from": {"box": "v", "name": "높이"},
"op": "<", "value": 1}},
],
"userElements": [],
} # fmt: skip
got = _post(client, "/api/m02/df/save", {"logic": lg})
v = got["values"]
assert v["ports"]["m"] == {"관": 30000, "골라 쓰는 관": 45000} # 조건 찾기 D300 · 선택 = 기본
total = Decimal("0.7") + 30000 * Decimal("1.1") + 4 # 표에서 찾기 + 10% + 통과(4) + 막힘("")
assert v["ports"]["m"] == {
"관 자리": 30000,
"골라 쓰는 관": 45000,
} # 조건 찾기 D300 · 선택 = 기본
total = Decimal("0.5") + 30000 * Decimal("1.1") + 2 # 깬돌 · 높이 2 → 「~3」 줄 · +10% · 통과
assert _d(v["ports"]["f"]["합"]) == int(total) # 함수 출력 끝수 0 자리 버림
assert _d(v["cost"]["재료비"]) == int(total) // 10 * 10 # 출력 끝수 −1 자리 버림
assert _d(v["cost"]["값들"]["골라 쓴 관"]) == 45000 and v["errors"] == []
assert _d(v["cost"]["값들"]["골라 쓴 관"]) == 45000 and v["cost"]["값들"]["낮을 때만"] is None
assert [b["kind"] for b in v["breaks"]] == ["통과 안 됨"] and v["errors"] == []
cells = {b["id"]: b for b in got["logic"]["boxes"]}["f"]["func"]["sheet"]["칸"]
assert _d(cells["A1"]["값"]) == Decimal("0.7") and "A4" not in cells # 받는 칸에 값만
assert _d(cells["A1"]["값"]) == Decimal("0.5") and cells["B1"] == {
"식": "A1+A2+A3"
} # 받는 칸에 값만
designs = client.get("/api/m02/df/design-inputs", params={"key": got["key"]}).json()["designs"]
assert [(d["port"], d["kind"]) for d in designs] == [
("돌", "변수"), ("높이", "변수"), ("관경", "변수"), ("골라 쓰는 관", "재료 자리")] # fmt: skip
lg["boxes"][0]["input"]["variables"][2]["example"] = "D999" # 맞는 재료 없음 · 표에 없는 조건
lg["boxes"][0]["input"]["variables"][0]["example"] = "없는돌"
bad = _post(client, "/api/m02/df/save", {"logic": lg})["values"]["errors"]
assert any("맞는 재료 없음" in e for e in bad) and any("표에 없는 조건" in e for e in bad)
lg["boxes"][0]["input"]["variables"][1]["example"] = 3 # 「~3」 줄(위 끝 포함)
assert _d(
_post(client, "/api/m02/df/save", {"logic": lg})["values"]["ports"]["f"]["합"]
) == int(Decimal("0.5") + 33000 + 3)
lg["boxes"][0]["input"]["variables"][2]["example"] = "D999" # 맞는 재료 없음
lg["boxes"][0]["input"]["variables"][0]["example"] = "없는돌" # 표에 없는 조건 → 함수 멈춤
bad = _post(client, "/api/m02/df/save", {"logic": lg})["values"]
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
def test_표준_표_읽기_쓰기_처음값(client: TestClient, folder: Path) -> None: