fix(m01-sl): 자재 바깥 값 — 검색어 묶음으로도 단가를 찾음(기존 엔진 재료 줄과 같은 길)

- {kind: "자재", key} 의 key 가 「{」 로 시작하는 JSON 글(또는 묶음)이면 {구분, 상세구분, 검색어, 규격?} 로 풀어 /calc 재료 줄처럼(검색어 · 자동값 차례 · 단위 환산) · 아니면 키
- unit(그 줄 단위)을 주면 기존 로직 재료 줄과 같은 단가 · 못 찾으면 쉬운 말(맞는 자재 없음 · 단가가 자료에 비어 있음)
- Api SlQuery 에 unit? · 시험 — 검색어 묶음 단가 = /calc 그 줄 단가(글 · 묶음) · 저장 때도 같은 값

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 13:53:20 +09:00
co-authored by Claude Opus 5.5
parent a02ea70d2e
commit ab514e8d10
3 changed files with 105 additions and 6 deletions
+60
View File
@@ -304,3 +304,63 @@ def test_표_블록이_Table_Import_와_같은_열(client: TestClient) -> None:
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == rows
def _material_cond() -> tuple[dict, str]:
"""값 있는 자재품목 한 줄 → 검색어 묶음 · 단위(사본에서 읽음)."""
rows = json.loads((store.FOLDER / "재료_자재품목.json").read_text(encoding="utf-8"))["줄"]
row = next(r for r in rows if isinstance(r.get("물가자료"), (int, float)) and r.get("구분"))
cond = {
"구분": row["구분"],
"상세구분": row["상세구분"],
"검색어": f"{row['이름']} {row['규격'] or ''}".strip(),
}
return cond, row["단위"]
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
def test_자재_검색어_묶음_단가는_calc_재료_줄과_같음(client: TestClient) -> None:
cond, unit = _material_cond()
line = {
"종류": "재료",
"요소": cond,
"이름": "재료",
"단위": unit,
"수량": "1",
"비목": "재료비",
}
row = {"키": "", "원문번호": "", "이름": "견본", "결과단위": "원/㎡", "출처": "자체",
"입력": [],
"중간": [], "호표": [line], "덧줄": [], "끝수": None} # fmt: skip
want = _post(client, "/api/m01/calc", {"key": "", "inputs": {}, "row": row})["lines"][0]["단가"]
assert want is not None
text = json.dumps(cond, ensure_ascii=False) # sub6 가져오기 꼴 — 묶음 JSON 글
bad = json.dumps({**cond, "검색어": "없는품목가나다"}, ensure_ascii=False)
items = [
{"id": "s", "kind": "자재", "key": text, "unit": unit},
{"id": "d", "kind": "자재", "key": cond, "unit": unit},
{"id": "x", "kind": "자재", "key": bad, "unit": unit},
]
got = {v["id"]: v for v in _post(client, "/api/m01/sl/outside", {"items": items})["values"]}
assert got["s"]["ok"] and got["s"]["value"] == want == got["d"]["value"]
assert got["x"]["ok"] is False and got["x"]["reason"] == "「없는품목가나다」 맞는 자재 없음"
# 저장 때도 같은 길 — Item · Import 꼴(몸의 outside 목록)
logic = {
"key": None,
"name": "자재 견본",
"resultUnit": "원/㎡",
"workbook": {
"종류": "통합문서",
"판": 1,
"서식": [{}],
"시트": [{"id": "s1", "이름": "재료", "칸": {}}],
},
"nodes": [],
"edges": [],
"outside": [{"item": items[0], "sheet": "s1", "cell": "B2"}],
}
saved = _post(client, "/api/m01/sl/save", {"logic": logic})
assert saved["logic"]["workbook"]["시트"][0]["칸"]["B2"]["값"] == want
_post(client, "/api/m01/sl/delete", {"key": saved["key"]})