fix(M02): 흐름 셈 옛 셈과 같은 뜻 — 재료 값이 로직 입력(자재지역 · 계약종별) 읽음 · 빈 단가는 알림 · 표 범위 경계 칸 · 검색어 조건 찾기(PLAN 36-2 대조)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
@@ -113,10 +113,12 @@ def _element_value(
|
||||
) -> Any:
|
||||
"""요소 알약 값 — 재료는 방식 셋 · 그 밖은 그룹 값 · 설계 값이 이김(선택 자리 = 키 · 수)."""
|
||||
mode = el.get("mode") or "fixed"
|
||||
env = ctx.get("env") or {} # 로직 입력(자재지역 · 계약종별 …) — 옛 셈 재료 찾기가 읽음
|
||||
if group == "재료" and mode == "choice":
|
||||
if isinstance(given, (int, float, Decimal)) and not isinstance(given, bool):
|
||||
return given
|
||||
got = dv.slot_price({"mode": "choice", "default": el.get("default")}, pick=given or None)
|
||||
slot = {"mode": "choice", "default": el.get("default"), "inputs": env}
|
||||
got = dv.slot_price(slot, pick=given or None)
|
||||
elif group == "재료" and mode == "rule":
|
||||
rule = el.get("rule") or {}
|
||||
src = rule.get("from") or {}
|
||||
@@ -125,13 +127,16 @@ def _element_value(
|
||||
"scope": el.get("scope"),
|
||||
"field": rule.get("field"),
|
||||
"op": rule.get("op"),
|
||||
"ref": el.get("ref"),
|
||||
"unit": el.get("unit"),
|
||||
"inputs": env,
|
||||
}
|
||||
got = dv.slot_price(slot, ports.get(src.get("box"), {}).get(src.get("name")))
|
||||
if not got["ok"] and el.get("default"):
|
||||
if not got["ok"] and not got.get("blank") and el.get("default"):
|
||||
errors.append(
|
||||
f"「{box.get('name')} · {el.get('name')}」 {got['reason']} — 기본 품목으로 셈"
|
||||
)
|
||||
got = dv.slot_price({"mode": "fixed", "ref": el["default"]})
|
||||
got = dv.slot_price({"mode": "fixed", "ref": el["default"], "inputs": env})
|
||||
elif group in dv.GROUP_RULE and mode == "rule": # 인력 · 기계 · 유가전력 조건 찾기
|
||||
rule = el.get("rule") or {}
|
||||
src = rule.get("from") or {}
|
||||
@@ -144,9 +149,11 @@ def _element_value(
|
||||
elif given is not None:
|
||||
return given
|
||||
else:
|
||||
got = dv.one_value(
|
||||
{"group": group, "ref": el.get("ref"), "unit": el.get("unit")}, ctx["local"]
|
||||
)
|
||||
item = {"group": group, "ref": el.get("ref"), "unit": el.get("unit"), "inputs": env}
|
||||
got = dv.one_value(item, ctx["local"])
|
||||
if not got["ok"] and got.get("blank"): # 옛 셈처럼 그 값만 비움(알림) — 셈은 계속
|
||||
ctx["notes"].append(f"「{box.get('name')} · {el.get('name')}」 {got.get('reason')}")
|
||||
return None
|
||||
if not got["ok"]:
|
||||
errors.append(f"「{box.get('name')} · {el.get('name')}」 {got.get('reason')}")
|
||||
return None
|
||||
@@ -487,7 +494,8 @@ NA_VALUE = "#N/A" # 함수 칸으로 가는 표 찾기 실패 — 칸에 NA()
|
||||
|
||||
def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict:
|
||||
"""셈 — {ports, outputs, cost, errors, breaks, filled}(filled = 받는 칸에 값만 되돌려 넣음)."""
|
||||
ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""]}
|
||||
ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""], "notes": [],
|
||||
"env": _env(lg, values)} # fmt: skip
|
||||
boxes = {b["id"]: b for b in lg.get("boxes") or []}
|
||||
links = lg.get("links") or []
|
||||
name = lambda i: boxes.get(i, {}).get("name") or i # noqa: E731
|
||||
@@ -640,4 +648,15 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
if b["id"] in sheets:
|
||||
b["func"]["sheet"] = sheets[b["id"]]
|
||||
return {"ports": ports, "arrived": arrived, "outputs": outputs, "cost": cost, "errors": errors,
|
||||
"breaks": breaks, "filled": filled} # fmt: skip
|
||||
"breaks": breaks, "notes": ctx["notes"], "filled": filled} # fmt: skip
|
||||
|
||||
|
||||
def _env(lg: dict, values: dict) -> dict:
|
||||
"""입력 상자 변수 값(설계 값 · 없으면 예시값) — 재료 찾기가 읽는 로직 입력."""
|
||||
return {
|
||||
v.get("name"): (values.get(b["id"]) or {}).get(v.get("name"), v.get("example"))
|
||||
for b in lg.get("boxes") or []
|
||||
if b.get("kind") == "input"
|
||||
for v in (b.get("input") or {}).get("variables") or []
|
||||
if v.get("name")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ from M01_MasterData import M01_MasterData_Store as store
|
||||
cm = store.cm
|
||||
_UKEY = re.compile(r"^UE-(\d{4,})$")
|
||||
RANGE_LOW, RANGE_HIGH = "·아래", "·위"
|
||||
RANGE_RULE = "범위규칙" # 표 블록 끝 칸 — 범위 경계(옛 find 와 같음 · 없으면 초과·이하)
|
||||
FIELDS = (*cm.mf.COST_ITEMS, "계")
|
||||
|
||||
|
||||
@@ -93,10 +94,12 @@ def _pick(key: Any) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _material(cond: dict, unit: str) -> dict:
|
||||
"""검색어 묶음 재료 단가 — 기존 로직 재료 줄과 같은 길(검색어 · 자동값 차례 · 단위 환산)."""
|
||||
def _material(cond: dict, unit: str, inputs: dict | None = None) -> dict:
|
||||
"""검색어 묶음 재료 단가 — 기존 로직 재료 줄과 같은 길(검색어 · 자동값 차례 · 단위 환산)
|
||||
· inputs = 로직 입력(자재지역 · 계약종별 · 검색어 {자리}) — 옛 셈이 읽는 그대로."""
|
||||
inputs = inputs or {}
|
||||
line = {"종류": "재료", "요소": cond, "이름": cond.get("검색어") or "재료", "수량": "1"}
|
||||
got = _calc({}, _row({**line, "단위": unit, "비목": "재료비"}, ()))
|
||||
got = _calc(inputs, _row({**line, "단위": unit, "비목": "재료비"}, inputs))
|
||||
if not got["ok"]:
|
||||
return got
|
||||
one = got["lines"][0]
|
||||
@@ -108,7 +111,7 @@ def _material(cond: dict, unit: str) -> dict:
|
||||
if empty
|
||||
else "맞는 재료 없음"
|
||||
)
|
||||
return {"ok": False, "reason": f"「{word}」 {why}"}
|
||||
return {"ok": False, "blank": True, "reason": f"「{word}」 {why} — {one.get('까닭') or ''}"}
|
||||
picked = one.get("품목") or {}
|
||||
return {"ok": True, "value": one["단가"], "name": picked.get("이름"), "unit": unit or None}
|
||||
|
||||
@@ -125,7 +128,7 @@ def _brief(key: str, want: str) -> tuple[dict | None, str | None]:
|
||||
return row, None
|
||||
|
||||
|
||||
def _price(group: str, key: str, unit: str = "") -> dict:
|
||||
def _price(group: str, key: str, unit: str = "", inputs: dict | None = None) -> dict:
|
||||
"""인력 노임 · 재료 단가 — 준용 · 낮은 값 · 출처는 엔진 그대로."""
|
||||
row, why = _brief(key, group)
|
||||
if row is None:
|
||||
@@ -133,11 +136,13 @@ def _price(group: str, key: str, unit: str = "") -> dict:
|
||||
kind, cost = {"인력": ("인력", "노무비"), "재료": ("재료", "재료비")}[group]
|
||||
line = {"종류": kind, "요소": key, "이름": row.get("이름") or key, "수량": "1"}
|
||||
line = {**line, "단위": unit or row.get("단위") or "", "비목": cost}
|
||||
got = _calc({}, _row(line, ()))
|
||||
inputs = inputs or {}
|
||||
got = _calc(inputs, _row(line, inputs))
|
||||
if not got["ok"]:
|
||||
return got
|
||||
one = got["lines"][0]
|
||||
out = {"ok": one["단가"] is not None, "value": one["단가"], "name": row.get("이름")}
|
||||
out |= {"blank": True} if one["단가"] is None else {} # 옛 셈은 그 줄만 비우고 계속
|
||||
return {**out, "unit": row.get("단위"), **({"reason": one["까닭"]} if one.get("까닭") else {})}
|
||||
|
||||
|
||||
@@ -209,6 +214,11 @@ def _table_of(table: dict | None, key: str, at: Any) -> dict:
|
||||
else:
|
||||
line.append(None if v == "" else v if not isinstance(v, dict) else str(v))
|
||||
rows.append(line)
|
||||
if (
|
||||
"범위" in conds.values()
|
||||
): # 범위 경계 규칙 — 끝 칸 「범위규칙」 줄마다 같은 글(lookup 이 읽음)
|
||||
rule = str(table.get(RANGE_RULE) or "초과·이하")
|
||||
head, rows = [*head, RANGE_RULE], [[*r, rule] for r in rows]
|
||||
return {"ok": True, "rows": [head, *rows], "name": table.get("이름"), "unit": table.get("기준")}
|
||||
|
||||
|
||||
@@ -304,9 +314,9 @@ def one_value(item: dict, local: list[dict] | None = None) -> dict:
|
||||
elif group in ("소요량", "계수"):
|
||||
got = _table(str(table.get("key") or ref or ""), table.get("row"))
|
||||
elif group == "재료" and _pick(ref) is not None:
|
||||
got = _material(_pick(ref), str(item.get("unit") or ""))
|
||||
got = _material(_pick(ref), str(item.get("unit") or ""), item.get("inputs"))
|
||||
elif group in ("인력", "재료"):
|
||||
got = _price(group, str(ref or ""), str(item.get("unit") or ""))
|
||||
got = _price(group, str(ref or ""), str(item.get("unit") or ""), item.get("inputs"))
|
||||
elif group == "기계" and str(ref or "").startswith("로직("):
|
||||
inputs = item.get("inputs") or {}
|
||||
line = {"종류": "로직", "요소": ref, "이름": "기계", "단위": "시간", "수량": "1"}
|
||||
@@ -387,7 +397,28 @@ def materials(q: str, sub: str, detail: str, page: int, size: int) -> dict:
|
||||
return {"total": len(hits), "page": page, "size": size, "items": [_brief_row(r) for r in part]}
|
||||
|
||||
|
||||
FIELDS_RULE, OPS_RULE = ("규격", "이름", "키"), ("포함", "같음") # Model CfMaterialRule
|
||||
FIELDS_RULE, OPS_RULE = ("규격", "이름", "키", "검색어"), ("포함", "같음") # Model CfMaterialRule
|
||||
_HOLE = re.compile(r"\{[^{}]+\}")
|
||||
|
||||
|
||||
def _filled(template: str, value: Any) -> str:
|
||||
"""검색어 틀의 {자리} — 알약 값이 이미 채운 글이면 그대로 · 아니면 자리마다 그 값."""
|
||||
text = str(value)
|
||||
pattern = "".join(
|
||||
".+" if _HOLE.fullmatch(p) else re.escape(p) for p in re.split(r"(\{[^{}]+\})", template)
|
||||
)
|
||||
return text if re.fullmatch(pattern, text) else _HOLE.sub(lambda _: text, template)
|
||||
|
||||
|
||||
def _search(slot: dict, value: Any) -> dict:
|
||||
"""조건 찾기 field 검색어 — 틀(요소 ref 묶음)의 {자리} 를 알약 값으로 채워 옛 찾기 그대로."""
|
||||
cond = _pick(slot.get("ref"))
|
||||
if cond is None:
|
||||
return {"ok": False, "reason": "검색어 틀이 없음 — 요소에 검색어 묶음을 넣어 주세요"}
|
||||
if value in (None, ""):
|
||||
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
|
||||
fill = {k: _filled(v, value) for k, v in cond.items() if isinstance(v, str) and _HOLE.search(v)}
|
||||
return _material({**cond, **fill}, str(slot.get("unit") or ""), slot.get("inputs"))
|
||||
|
||||
|
||||
def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
@@ -397,14 +428,14 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
scope?: {구분?, 상세구분?, 찾는말?}, field?: 규격 | 이름, op?: 포함 | 같음}
|
||||
조건 찾기(value = 이어진 알약 값): 하나면 그 단가 · 0 이면 맞는 재료 없음 · 여럿이면 후보 다섯.
|
||||
"""
|
||||
mode = slot.get("mode") or "fixed"
|
||||
mode, inputs = slot.get("mode") or "fixed", slot.get("inputs")
|
||||
if mode == "fixed":
|
||||
return _price("재료", str(slot.get("ref") or ""))
|
||||
return _price("재료", str(slot.get("ref") or ""), "", inputs)
|
||||
if mode == "choice":
|
||||
ref = pick or slot.get("default")
|
||||
if not ref:
|
||||
return {"ok": False, "reason": "고른 품목도 기본 품목도 없음 — 품목을 골라 주세요"}
|
||||
return _price("재료", str(ref))
|
||||
return _price("재료", str(ref), "", inputs)
|
||||
if mode != "rule":
|
||||
return {"ok": False, "reason": f"모르는 재료 방식 「{mode}」"}
|
||||
field, op, scope = (
|
||||
@@ -414,6 +445,8 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
)
|
||||
if field not in FIELDS_RULE or op not in OPS_RULE:
|
||||
return {"ok": False, "reason": f"모르는 찾기 규칙 「{field} {op}」"}
|
||||
if field == "검색어":
|
||||
return _search(slot, value)
|
||||
if value in (None, ""):
|
||||
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
|
||||
want = _squash(value)
|
||||
@@ -436,7 +469,7 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
"reason": f"맞는 재료 {len(hits)}개 — 범위나 규칙을 좁혀 주세요({names} …)",
|
||||
"candidates": [_brief_row(r) for r in hits[:5]],
|
||||
}
|
||||
got = _price("재료", str(hits[0].get("키")))
|
||||
got = _price("재료", str(hits[0].get("키")), "", inputs)
|
||||
return {**got, "ref": hits[0].get("키"), "name": hits[0].get("이름")}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user