# -*- coding: utf-8 -*- """수식 변수 연결 지정(PLAN 800-3 · 요구 660-5) — 새 기호가 무엇과 이어지는지 고르는 서버 몫. 고침 `{할일: 연결, 열쇠, 갈래, 이름?, 기호?, 위?, 아래?, 뒤?, …}` 을 기존 `더함` 한 건으로 풂(`plan`): 입력 {기본값?, 단위?} → 입력 변수(옵션 칸에 생김) 표 {표, 칸, 조건: {열: 식글}} → 중간 줄 「찾기(표, 열=식…).칸」 요소 {요소: 키} → 중간 줄 「키」(인력 · 재료 · 기계 단가) 로직 {로직: 키, 인자?: {칸: 식}} → 중간 줄 「로직(키, 칸=식…)」 상수 {값: 수} → 중간 줄 수 후보 목록 = `candidates`(표 · 요소 · 로직) — 읽기만 · 값 · 열쇠는 이름 옆 보관용(화면은 이름만 보임). 셈 · 검사는 `master_equation_edit.apply` 가 한 벌로(여기는 글만 짓고 막을 것만 막음). """ from __future__ import annotations import re from decimal import Decimal, InvalidOperation LIMIT = 30 KINDS = ("입력", "표", "요소", "로직", "상수") ELEMS = {"인력": ("LB",), "재료": ("MT", "MO"), "기계": ("EQ",)} TABLES = ("QC", "QF") _KEY = re.compile(r"[A-Z]{2}\d{6}") _COL = re.compile(r"^[^\W\d][\w]*$") def _q(word: str) -> str: return word if _COL.match(word) else f"'{word}'" def _row(master, key: str): try: return master.get(key) except Exception: # noqa: BLE001 — 마스터 없는 열쇠 = 어떤 오류든 「없음」 return None def plan(master, ed: dict) -> tuple[dict | None, str]: """연결 고침 → (더함 고침, 까닭) — 까닭이 있으면 더함 없음.""" kind = ed.get("갈래") add = { "할일": "더함", **{k: ed[k] for k in ("열쇠", "이름", "기호", "위", "아래", "단위", "뒤") if k in ed}, } if kind not in KINDS: return None, f"연결 갈래 「{kind}」 — {' · '.join(KINDS)}" if kind == "입력": add |= {"갈래": "입력"} | ( {"기본값": ed["기본값"]} if ed.get("기본값") not in (None, "") else {} ) return add, "" add["갈래"] = "중간" if kind == "상수": try: add["식"] = format(Decimal(str(ed.get("값"))).normalize(), "f") except (InvalidOperation, ValueError): return None, "상수는 수로 적어야 함" elif kind == "요소": key = str(ed.get("요소") or "") if ( not _KEY.fullmatch(key) or key[:2] not in sum(ELEMS.values(), ()) or not _row(master, key) ): return None, "요소를 골라야 함(인력 · 재료 · 기계)" add["식"] = key elif kind == "로직": key = str(ed.get("로직") or "") if not _KEY.fullmatch(key) or not key.startswith("G") or not _row(master, key): return None, "부를 로직을 골라야 함" args = "".join( f", {_q(k)}={v}" for k, v in (ed.get("인자") or {}).items() if str(v).strip() ) add["식"] = f"로직({key}{args})" else: key, col = str(ed.get("표") or ""), str(ed.get("칸") or "") table = _row(master, key) if _KEY.fullmatch(key) else None if not table or "조건" not in table or col not in (table.get("값칸") or {}): return None, "표와 칸을 골라야 함" given = ed.get("조건") or {} miss = [c for c in table["조건"] if not str(given.get(c) or "").strip()] if miss: return None, f"표 조건을 채워야 함 — {' · '.join(miss)}" cond = "".join(f", {_q(c)}={given[c]}" for c in table["조건"]) add["식"] = f"찾기({key}{cond}).{_q(col)}" return add, "" def _hit(text: object, words: list[str]) -> bool: low = str(text or "").lower() return all(w in low for w in words) def candidates(master, ask: dict) -> dict: """후보 {갈래: 표 | 요소 | 로직, 검색, 분류?(요소)} → {후보: [{키, 이름, …}]} — 최대 `LIMIT` 건.""" kind, words = ask.get("갈래"), str(ask.get("검색") or "").lower().split() out: list[dict] = [] if kind == "표": pools = [(k, r) for p in TABLES for k, r in master.index.get(p, {}).items() if "조건" in r] for key, r in pools: if _hit(r.get("이름"), words): out.append( { "키": key, "이름": r.get("이름"), "조건": list(r["조건"]), "칸": list(r.get("값칸") or {}), } ) elif kind == "요소": for p in ELEMS.get(str(ask.get("분류") or "인력"), ()): for key, r in master.index.get(p, {}).items(): if (words or p != "MT") and _hit(f"{r.get('이름')} {r.get('규격') or ''}", words): out.append({"키": key, "이름": r.get("이름"), "규격": r.get("규격") or ""}) elif kind == "로직": for key, r in master.index.get("GC", {}).items(): if words and _hit(r.get("이름"), words): out.append( { "키": key, "이름": r.get("이름"), "입력": [s["이름"] for s in r.get("입력") or []], } ) return {"후보": out[:LIMIT], "더있음": len(out) > LIMIT}