fix(M02): 흐름 셈 = 옛 셈 — 표 「·범위규칙」 줄 경계 말 · 검색어 조건 찾기 · 재료가 로직 입력 읽음 · 빈 단가 알림 · 고르기 글 값 그대로(PLAN 36-2 대조 49 → 8)

- main(sub7 a774b4e97 Model 덧) 받아 합침 · Compute 재료 조건 찾기는 sub7 의 틀 채우기 + 빈 단가 알림
- 남은 8: 표에 여럿 맞음 5(옛 find 의 값 칸 있는 줄만 거름 — lookup) · 빈 값이 든 식 3(옛 = 그 줄 비움 · 흐름 = 빈 칸 0)

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-02 01:04:03 +09:00
co-authored by Claude Opus 5.5
16 changed files with 345 additions and 42 deletions
+26 -10
View File
@@ -19,7 +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 와 같음 · 없으면 초과·이하)
RANGE_RULE = "·범위규칙" # 표 블록 끝 칸 — 줄마다 경계 말(옛 find 와 같음 · 없으면 초과·이하)
FIELDS = (*cm.mf.COST_ITEMS, "계")
@@ -214,14 +214,28 @@ 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]
if "범위" in conds.values(): # 끝 칸 「·범위규칙」 = 줄마다 그 줄 경계 말(lookup 이 읽음)
head, rows = [*head, RANGE_RULE], [[*r, _bound(table, x)] for r, x in zip(rows, lines)]
return {"ok": True, "rows": [head, *rows], "name": table.get("이름"), "unit": table.get("기준")}
def _bound(table: dict, row: dict) -> str:
"""줄 경계 말 — 옛 `_match` 와 같음 · 첫 낱말 · 「이하 포함」 이면 맨 윗줄만 이상·이하."""
rule, *more = str(table.get("범위규칙") or "초과·이하").split(" · ")
rule = rule.split(" (")[0]
if rule != "이상·미만" or not any("이하 포함" in x for x in more):
return rule
for c, kind in (table.get("조건") or {}).items(): # 옛 _match — 그 범위 칸 위 끝이 가장 큰 줄
ups = [
r[c][1]
for r in table.get("줄") or []
if isinstance(r.get(c), list) and r[c][1] is not None
]
if kind == "범위" and ups and isinstance(row.get(c), list) and row[c][1] == max(ups):
return "이상·이하"
return rule
def _arith(v: Any) -> Any:
"""값 칸 수 식 글(「(0.05)」 「1/1.30」) — 옛 셈처럼 수로(사칙연산 · 괄호만 · 옛 엔진 파서)."""
if not isinstance(v, str) or not cm.mf._ARITH.fullmatch(v):
@@ -412,13 +426,15 @@ def _filled(template: str, value: Any) -> str:
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": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
cond = _pick(slot.get("ref")) # 요소 ref = 옛 검색어 묶음(규격 · 지역 칸도 그대로)
if cond is None: # 묶음이 없으면 범위(구분 · 상세구분) + 채운 검색어
scope = slot.get("scope") or {}
cond = {k: scope[k] for k in ("구분", "상세구분") if scope.get(k)} | {"검색어": ""}
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"))
cond = {**cond, **fill} if fill else {**cond, "검색어": str(value)}
return _material(cond, str(slot.get("unit") or ""), slot.get("inputs"))
def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict: