feat(800-2): 수식 변수 정의 영역 서버 — 정의 칸(안 탄 갈래 뺌 · 겹침 합침 · 열쇠 글 0) · 새 기호 연결 고침 · 후보 길

- 수식 응답 새 칸 정의 — 만약 탄 갈래 표지 걸림 그대로 · 같은 기호 한 줄 · 연결 사람 말 · 값 · 단위 · DB 열쇠 없음
- 고침 할일 연결(입력 · 표 · 요소 · 로직 · 상수) · 모르는 기호 까닭 · 후보 길 M01 · M02 equation/links
- 낡은 시험 test_796_1 「모르는 이름」 → 「모르는 기호」(까닭 문구 바뀜)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 16:46:39 +09:00
co-authored by Claude Sonnet 5.5
parent 972002417a
commit a262b43e83
10 changed files with 448 additions and 1 deletions
@@ -0,0 +1,125 @@
# -*- 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}