From 9872ad66f58ca3841a22b8b69e716a7e4e33050a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 2 Oct 2026 00:35:21 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=ED=9D=90=EB=A6=84=20=EC=85=88=20?= =?UTF-8?q?=EC=98=9B=20=EC=85=88=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EB=9C=BB?= =?UTF-8?q?=20=E2=80=94=20=EC=9E=AC=EB=A3=8C=20=EA=B0=92=EC=9D=B4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9E=85=EB=A0=A5(=EC=9E=90=EC=9E=AC?= =?UTF-8?q?=EC=A7=80=EC=97=AD=20=C2=B7=20=EA=B3=84=EC=95=BD=EC=A2=85?= =?UTF-8?q?=EB=B3=84)=20=EC=9D=BD=EC=9D=8C=20=C2=B7=20=EB=B9=88=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EB=8A=94=20=EC=95=8C=EB=A6=BC=20=C2=B7=20?= =?UTF-8?q?=ED=91=9C=20=EB=B2=94=EC=9C=84=20=EA=B2=BD=EA=B3=84=20=EC=B9=B8?= =?UTF-8?q?=20=C2=B7=20=EA=B2=80=EC=83=89=EC=96=B4=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0(PLAN=2036-2=20=EB=8C=80=EC=A1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- M02_MasterTemplete/DataFlow/M02_DF_Compute.py | 35 ++++++++--- M02_MasterTemplete/DataFlow/M02_DF_Values.py | 59 +++++++++++++++---- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py index 1cfd10991..f82a20085 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Compute.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Compute.py @@ -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") + } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Values.py b/M02_MasterTemplete/DataFlow/M02_DF_Values.py index 0cc31950f..ab2a20e59 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Values.py +++ b/M02_MasterTemplete/DataFlow/M02_DF_Values.py @@ -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("이름")}