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:
@@ -17,8 +17,9 @@ import type { SheetLogic, SlField, SlSource } from "./M01_SL_Model";
|
||||
|
||||
export type { SlField };
|
||||
|
||||
/** 서버 `/outside` 물음 한 줄 = `{id, ...SlSource}` */
|
||||
export type SlQuery = { id: string } & SlSource;
|
||||
/** 서버 `/outside` 물음 한 줄 = `{id, ...SlSource}` · 자재 `key` = 키 또는 검색어 묶음 JSON 글
|
||||
* (`{구분, 상세구분, 검색어, 규격?}`) · `unit` = 그 줄 단위(주면 기존 로직 재료 줄과 같은 단가) */
|
||||
export type SlQuery = { id: string; unit?: string } & SlSource;
|
||||
/** Item 이 쓰는 옛 이름 — `SlQuery` 와 같음(Model `SlOutside` 와 다른 것) */
|
||||
export type SlOutside = SlQuery;
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def delete(key: str) -> dict:
|
||||
# ── 바깥 값 ────────────────────────────────────────────────────────────
|
||||
# 물음 한 줄 = `{id, ...SlSource}`(Model) · 답 = Eval `SlOutsideAnswer {id, ok, value | rows}`
|
||||
# 표 {kind: "표", table} → rows = 머리 줄 + 줄(범위는 「이름·아래」 「이름·위」 — `rangeHead`)
|
||||
# 노임 · 자재 {kind, key} → 그 줄 단가(`/calc` 와 같은 셈 · 준용 · 낮은 값)
|
||||
# 노임 · 자재 {kind, key, unit?} → 그 줄 단가(`/calc` 와 같은 셈) · 자재 key = 키 · 검색어 묶음
|
||||
# 장비 {kind: "장비", element, inputs, field?} → 기계경비 로직 부름 글 한 줄(수량 1)
|
||||
# 로직 {kind: "로직", key, inputs, field?} → 기존 로직 결과(빈 입력은 자동값으로 채움)
|
||||
RANGE_LOW, RANGE_HIGH = "·아래", "·위" # Model `RANGE_LOW` · `RANGE_HIGH` 와 같음
|
||||
@@ -131,8 +131,46 @@ def _money(got: dict, field: str | None) -> dict:
|
||||
return {"ok": True, "value": got["sums"][pick]}
|
||||
|
||||
|
||||
def _price(kind: str, key: str) -> dict:
|
||||
"""노임 · 자재 한 줄 단가 — 준용 · 낮은 값 · 출처는 엔진 그대로."""
|
||||
def _pick(key: Any) -> dict | None:
|
||||
"""자재 검색어 묶음 `{구분, 상세구분, 검색어, 규격?}` — 묶음 · 그 JSON 글 · 아니면 None."""
|
||||
if isinstance(key, dict):
|
||||
return key
|
||||
if isinstance(key, str) and key.lstrip().startswith("{"):
|
||||
try:
|
||||
got = json.loads(key)
|
||||
except ValueError:
|
||||
return None
|
||||
return got if isinstance(got, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _material(cond: dict, unit: str) -> dict:
|
||||
"""검색어 묶음 자재 단가 — 기존 로직 재료 줄과 같은 길(검색어 · 자동값 차례 · 단위 환산)."""
|
||||
line = {"종류": "재료", "요소": cond, "이름": cond.get("검색어") or "재료", "수량": "1"}
|
||||
got = store.calc("", {}, _row({**line, "단위": unit, "비목": "재료비"}, ()), None)
|
||||
if not got["ok"]:
|
||||
return got
|
||||
one = got["lines"][0]
|
||||
if one["단가"] is None:
|
||||
word = cond.get("검색어") or ""
|
||||
empty = "값 없음" in str(one.get("까닭") or "") # 품목은 찾았는데 단가 칸이 빔
|
||||
why = (
|
||||
"단가가 자료에 비어 있음 — 단가를 채우거나 다른 품목을 고름"
|
||||
if empty
|
||||
else "맞는 자재 없음"
|
||||
)
|
||||
return {"ok": False, "reason": f"「{word}」 {why}"}
|
||||
picked = one.get("품목") or {}
|
||||
out = {"ok": True, "value": one["단가"], "name": picked.get("이름"), "unit": unit or None}
|
||||
return {**out, **({"reason": one["단위경고"]} if one.get("단위경고") else {})}
|
||||
|
||||
|
||||
def _price(kind: str, key: Any, unit: str = "") -> dict:
|
||||
"""노임 · 자재 한 줄 단가 — 준용 · 낮은 값 · 출처는 엔진 그대로 · 자재는 키 또는 검색어 묶음."""
|
||||
cond = _pick(key) if kind == "자재" else None
|
||||
if cond is not None:
|
||||
return _material(cond, unit)
|
||||
key = str(key or "")
|
||||
try:
|
||||
brief = store.loaded()[1].get(key)
|
||||
except cm.mf.FormulaError as e:
|
||||
@@ -205,7 +243,7 @@ def one_outside(item: dict) -> dict:
|
||||
if kind == "표":
|
||||
got = _table(str(item.get("table") or ""))
|
||||
elif kind in ("노임", "자재"):
|
||||
got = _price(kind, str(item.get("key") or ""))
|
||||
got = _price(kind, item.get("key"), str(item.get("unit") or ""))
|
||||
elif kind == "장비":
|
||||
inputs = item.get("inputs") or {}
|
||||
line = {"종류": "로직", "요소": item.get("element") or "", "이름": "장비"}
|
||||
|
||||
@@ -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"]})
|
||||
|
||||
Reference in New Issue
Block a user