diff --git a/M01_MasterData/M01_MasterData_Equation.py b/M01_MasterData/M01_MasterData_Equation.py index fa08f998f..7b390fdab 100644 --- a/M01_MasterData/M01_MasterData_Equation.py +++ b/M01_MasterData/M01_MasterData_Equation.py @@ -16,6 +16,7 @@ from M01_MasterData import M01_MasterData_Store_Make as make _ERR = (store.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) import master_equation as meq # noqa: E402 — `store` 가 scripts 를 길에 둔 뒤 +import master_equation_link as link # noqa: E402 import master_equation_edit as me # noqa: E402 @@ -81,6 +82,11 @@ def preview(key: str, edits: list, inputs: dict | None = None) -> dict: return {"되나": True, "까닭": [], "줄": new, "수식": eq, "전": before, "후": eq["합계"]} +def links(ask: dict) -> dict: + """새 기호 연결 후보(800-3) — {갈래: 표 | 요소 | 로직, 검색, 분류?} → {후보, 더있음} · 읽기만.""" + return link.candidates(store.loaded()[1], ask) + + def save( key: str, edits: list, inputs: dict | None = None, version: str = "", confirmed: bool = False ) -> dict: diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index 50741d008..e55251cb7 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -372,6 +372,18 @@ def post_logic_equation_preview(body: EquationBody) -> dict: return _call(equation.preview, body.key, body.고침, body.inputs) +class LinksBody(BaseModel): + 갈래: str + 검색: str = "" + 분류: str = "인력" + + +@router.post("/logic/equation/links") +def post_logic_equation_links(body: LinksBody) -> dict: + """새 기호 연결 후보(800-3) — 표 · 요소 · 로직 이름 찾기(읽기만).""" + return _call(equation.links, body.model_dump()) + + @router.post("/logic/equation/save") def post_logic_equation_save(body: EquationBody) -> dict: """수식 고침 저장 — 검사 통과 뒤 `/save` 길(정본 = 개정확인 · 판 다르면 409).""" diff --git a/M02_MasterTemplete/M02_Sangeun_Equation.py b/M02_MasterTemplete/M02_Sangeun_Equation.py index 45d8de546..de76427d3 100644 --- a/M02_MasterTemplete/M02_Sangeun_Equation.py +++ b/M02_MasterTemplete/M02_Sangeun_Equation.py @@ -163,6 +163,11 @@ def read(doc: Any, inputs: dict | None = None) -> dict[str, Any]: return out | ({"reason": lost[0]["까닭"]} if lost else {}) +def links(doc: Any, ask: dict) -> dict[str, Any]: + """새 기호 연결 후보(800-3) — M01 과 같은 후보 길(읽기만).""" + return _eqn().links(ask) + + def preview(doc: Any, edits: list, inputs: dict | None = None) -> dict[str, Any]: """고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(고침 없는 사본 SK 금액), 후}.""" from M02_MasterTemplete import M02_Sangeun_Equation_Full as full diff --git a/M02_MasterTemplete/M02_Sangeun_Router.py b/M02_MasterTemplete/M02_Sangeun_Router.py index 6ab140d92..e87d765db 100644 --- a/M02_MasterTemplete/M02_Sangeun_Router.py +++ b/M02_MasterTemplete/M02_Sangeun_Router.py @@ -251,6 +251,26 @@ def sangeun_equation( return _call(_root(session, layer, owner), _sd(eqm.read), body.문서, body.inputs) +class LinksBody(BaseModel): + 문서: dict[str, Any] | None = None + 갈래: str + 검색: str = "" + 분류: str = "인력" + 블록: int | None = None + + +@router.post("/layers/{layer}/sangeun/equation/links") +def sangeun_equation_links( + layer: Layer, + body: LinksBody, + owner: str | None = None, + session: Session = Depends(verify_session), +) -> dict: + """새 기호 연결 후보(800-3) — 표 · 요소 · 로직 이름 찾기(읽기만).""" + ask = {k: getattr(body, k) for k in ("갈래", "검색", "분류")} + return _call(_root(session, layer, owner), _sd(eqm.links), body.문서 or {}, ask) + + @router.post("/layers/{layer}/sangeun/equation/preview") def sangeun_equation_preview( layer: Layer, diff --git a/resources/master_data/scripts/master_equation.py b/resources/master_data/scripts/master_equation.py index 7514473a4..a050c0896 100644 --- a/resources/master_data/scripts/master_equation.py +++ b/resources/master_data/scripts/master_equation.py @@ -28,7 +28,9 @@ from pathlib import Path import master_auto as ma import master_cut as mcut +import master_equation_view as view import master_formula as mf +import master_material as mm from common_util.common_util_money import fmt @@ -195,10 +197,18 @@ def _raw(node) -> str: return me.raw_text(node) +def me_keyboard(ctx: _Ctx, node) -> str: + """조건 식 마디 → 키보드 한 줄 글(변수 = 기호) — 정의 영역 「조건 규격 = 폼타이규격」.""" + import master_equation_edit as me + + return me.keyboard_of(_raw(node), ctx.row) + + def _find(ctx: _Ctx, node, column: str) -> dict: """찾기(표, 조건).칸 → 찾기 변수 · 표 · 고른 행.""" raw = _raw(("field", node, column)) more = {"식": raw, "칸": column} + more["조건식"] = {k: me_keyboard(ctx, c) for k, c in node[2]} try: ref = mf.fill(node[1], ctx.env) table_row = ctx.master.get(ref) @@ -451,6 +461,11 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict: total = {k: cut.get(k) for k in mf.BUNDLE} if depth == 0: sub = {"소계": subtotals(row, cut, total)} + defs = ( + {"정의": view.definitions(out_lines, row, table(row, master), _unused(row))} + if depth == 0 + else {} + ) return _clean( { "열쇠": row.get("키"), @@ -461,10 +476,18 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict: "변수표": table(row, master), "합계": total, **sub, + **defs, } ) +def _unused(row: dict): + """열쇠 → 어느 식에도 안 쓰이나(정의 영역 · 지울 수 있는 입력).""" + import master_equation_edit as me + + return lambda key: key != mm.REGION and not me.uses(row, key) + + def _item(ctx: _Ctx, at: int, item: dict, line: dict) -> dict: """호표 줄 = 「단가 × 수량식 = 금액」 · 로직 줄 단가 = 블록.""" name = str(item.get("이름") or "") diff --git a/resources/master_data/scripts/master_equation_edit.py b/resources/master_data/scripts/master_equation_edit.py index 1c6c97771..2b35e6fe9 100644 --- a/resources/master_data/scripts/master_equation_edit.py +++ b/resources/master_data/scripts/master_equation_edit.py @@ -10,6 +10,7 @@ {할일: 지움, 열쇠} — 쓰는 식이 있으면 막고 쓰는 곳 목록 {할일: 변수, 열쇠, 이름?, 기호?, 위?, 아래?, 단위?} — 식은 열쇠로 부르니 안 깨짐 {할일: 표시, 열쇠, 표시: 0~6 | null} + {할일: 연결, 열쇠, 갈래: 입력|표|요소|로직|상수, …} — 새 기호의 연결 지정(`master_equation_link.py` 머리) 고친 중간 줄은 `표시` 없으면 `SHOW`(보이는 자리에서 잘라 셈 · 요구 658) — 고치지 않은 줄은 그대로. 키보드 한 줄 식 = 변수 글(기호_아래^위 · 이름 · 열쇠) · `× ÷ −` · 나머지는 엔진 식 말 그대로 (`[노임 …]` · `[별칭]` 은 M01 · M02 층이 `M02_Sangeun_Formula.to_engine` 으로 먼저 풂). @@ -22,6 +23,7 @@ import re from decimal import Decimal, InvalidOperation import master_equation as meq +import master_equation_link as link import master_formula as mf KEY_FORM = re.compile(r"^[^\W\d][\w]{0,19}$") # 열쇠 = 글자로 시작 · 20 자까지 @@ -227,12 +229,39 @@ def _text_of(ed: dict, row: dict, words) -> tuple[str, list[str]]: return words(text, row) if words and text else (text, []) +def unknown(text: str, row: dict) -> list[str]: + """엔진 식 글 안 모르는 변수 글(변수 · 열쇠 · 예약어 · 함수 · 칸 · 인자 이름 · 요소 키 빼고).""" + known = set(_names(row)[0]) | _all_keys(row) | RESERVED + out, at = [], 0 + while at < len(text): + if text[at] in "'[": + end = text.find("'" if text[at] == "'" else "]", at + 1) + at = len(text) if end < 0 else end + 1 + continue + m = None if at and text[at - 1].isalnum() else _TOKEN.match(text, at) + if not m: + at += 1 + continue + word, before, rest = m.group(), text[:at].rstrip(), text[m.end() :].lstrip() + at = m.end() + arg = rest.startswith("=") and not rest.startswith("==") + skip = before.endswith(".") or rest.startswith("(") or arg + if not (skip or word in known or mf.mk.KEY.fullmatch(word) or word in out): + out.append(word) + return out + + def _set_formula(row: dict, ed: dict, why: list, spots: set, words=None) -> None: spot = str(ed.get("자리") or "") text, bad = _text_of(ed, row, words) if bad: why += [f"{spot} · {w}" for w in bad] return + if not isinstance(ed.get("나무"), dict) and (miss := unknown(text, row)): + why.append( + f"{spot} · 모르는 기호 「{'」 「'.join(miss)}」 — 변수 정의 영역에서 연결을 지정" + ) + return if not text: why.append(f"{spot} · 식이 빔") return @@ -383,6 +412,9 @@ def apply(master, row: dict, edits: list, words=None) -> tuple[dict, list[str], _look(new, key, ed, why) elif job == "표시": _show(new, key, ed, why) + elif job == "연결": + add, bad = link.plan(master, ed) + _add(new, add, why, spots, words) if add else why.append(bad) else: why.append(f"모르는 할일 「{job}」") except (mf.FormulaError, KeyError, TypeError, ValueError) as e: diff --git a/resources/master_data/scripts/master_equation_link.py b/resources/master_data/scripts/master_equation_link.py new file mode 100644 index 000000000..ed578c84b --- /dev/null +++ b/resources/master_data/scripts/master_equation_link.py @@ -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} diff --git a/resources/master_data/scripts/master_equation_view.py b/resources/master_data/scripts/master_equation_view.py new file mode 100644 index 000000000..ba49bb5f6 --- /dev/null +++ b/resources/master_data/scripts/master_equation_view.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""수식 변수 정의 영역(PLAN 800-2 · 요구 660-2 · 3 · 6) — 수식 나무 → 오른쪽 「변수 정의」 목록. + +줄 구분 없이 한 목록 · 같은 변수는 한 번만(겹침 합침) · 기호 : 국문 이름 — 연결(입력 · 표 찾기 · +요소 · 로직 · 계산을 사람 말로) · 값 · 단위 · DB 열쇠 글은 담지 않음. +지금 고른 옵션 갈래만 — 만약의 탄 갈래 표지 `걸림`(1 참 · 2 거짓 · 0 모름 = 둘 다)을 그대로 씀(새 판정 없음) · +`안씀` = 어느 식에도 안 쓰이는 입력(지울 수 있음) · 쓰는 변수 지움 막기는 `master_equation_edit._drop`. +""" + +from __future__ import annotations + +CAT = {"LB": "인력", "EQ": "기계", "MT": "재료", "MO": "재료"} +WHAT = { + "입력": "입력", + "중간": "계산", + "결과": "계산 결과", + "찾기": "표 찾기", + "요소": "요소", + "블록": "로직 부르기", +} + + +def visible(node: dict | None, out: list[dict]) -> list[dict]: + """나무에서 보이는(탄 갈래) 변수 · 블록 마디를 차례대로.""" + if not isinstance(node, dict): + return out + kind = node.get("꼴") + if kind in ("변수", "블록"): + out.append(node) + elif kind == "조건": + visible(node.get("조건"), out) + for key, at in (("참", 1), ("거짓", 2)): + if node.get("걸림") in (0, None, at): + visible(node.get(key), out) + else: + for key in ("좌", "우", "안", "위", "아래"): + visible(node.get(key), out) + for arg in node.get("인자") or []: + visible(arg, out) + return out + + +def _link(node: dict, spec: dict | None, leaf: dict | None) -> str: + """연결 설명 — 사람 말(열쇠 · 값 · 단위 없음).""" + kind = node.get("갈래") or ("블록" if node.get("꼴") == "블록" else "") + if kind == "입력": + return "입력 — " + ("옵션에서 고름" if (spec or {}).get("고르기") else "직접 적음") + if kind == "중간" and leaf is not None and leaf is not node: + return _link(leaf, None, None) + if kind == "찾기": + cond = " · ".join(f"{k} = {v}" for k, v in (node.get("조건식") or {}).items()) + head = f"표 찾기 — {node.get('표이름') or '표'}" + return head + (f" · 조건 {cond}" if cond else "") + f" · 칸 {node.get('칸') or ''}".rstrip() + if kind == "요소": + pick = node.get("고르기") + if isinstance(pick, dict): + return "요소 · 재료 고름 — " + " · ".join(str(v) for v in pick.values() if v) + return f"요소 · {CAT.get(str(node.get('식') or '')[:2], '단가')} — {node.get('이름') or ''}" + if kind == "블록": + return f"로직 부르기 — {node.get('이름') or ''}" + return WHAT.get(kind, "계산") + (" — 수식 영역의 식" if kind in ("중간", "결과") else "") + + +def definitions(lines: list[dict], row: dict, rows: list[dict], unused) -> list[dict]: + """보이는 식의 변수 정의 목록 — `rows` = 변수 표 · `unused(열쇠)` = 어느 식에도 안 쓰임.""" + specs = {str(s.get("이름")): s for k in ("입력", "중간") for s in row.get(k) or []} + table = {v["열쇠"]: v for v in rows} + got: dict[str, dict] = {} + leaf: dict[str, dict] = {} + for line in lines: + head, tree = line.get("결과"), line.get("이름식") + if line.get("갈래") in ("중간", "결과") and isinstance(tree, dict): + if tree.get("갈래") in ("찾기", "요소") or tree.get("꼴") == "블록": # 한 마디 식 + leaf[str(line.get("열쇠"))] = tree + found = ([head] if head and line.get("갈래") in ("중간", "결과") else []) + visible( + tree, [] + ) + for node in found: + key = str(node.get("열쇠") or "") + if key and key not in got and node.get("갈래") in (*WHAT, "합", None): + got[key] = node + out: dict[tuple, dict] = {} # 같은 기호 글 = 같은 정의(겹침 합침) — 연결은 모아 보임 + for key, node in got.items(): + mine = table.get(key) + look = mine or node + kind = node.get("갈래") or "블록" + if kind == "합": + kind = "결과" if key == "결과" else "중간" + if key.startswith("줄."): # 위 호표 줄 금액 부름 + name, link = key[2:].strip("'"), f"호표 줄 금액 — {key[2:].strip(chr(39))}" + else: + link = _link({**node, "갈래": kind}, specs.get(key), leaf.get(key)) + name = (look.get("이름") or key) if mine else look.get("이름") or "" + name = node.get("칸") or name if kind == "찾기" else name + one = { + "열쇠": key if mine else "", + "기호": (look.get("기호") or name) if not key.startswith("줄.") else name, + "위": look.get("위") or "", + "아래": look.get("아래") or "", + "이름": name, + "갈래": kind, + "연결": [link], + "고침": bool(mine), + } + same = out.setdefault((one["기호"], one["위"], one["아래"]), one) + if same is not one: + same["연결"] += [x for x in one["연결"] if x not in same["연결"]] + if one["열쇠"] and not same["열쇠"]: + same |= {k: one[k] for k in ("열쇠", "이름", "갈래", "고침")} + out = list(out.values()) + seen = {d["열쇠"] for d in out} + for v in rows: # 어느 식에도 안 쓰이는 입력 — 지울 수 있게 + if v["갈래"] == "입력" and v["열쇠"] not in seen and unused(v["열쇠"]): + out.append( + { + "열쇠": v["열쇠"], + "기호": v["기호"], + "위": v["위"], + "아래": v["아래"], + "이름": v["이름"], + "갈래": "입력", + "연결": ["입력 — 쓰는 식 없음"], + "고침": True, + "안씀": True, + } + ) + return out diff --git a/resources/tester/test_796_1_formula_model.py b/resources/tester/test_796_1_formula_model.py index 03615ce37..99d84038a 100644 --- a/resources/tester/test_796_1_formula_model.py +++ b/resources/tester/test_796_1_formula_model.py @@ -296,7 +296,7 @@ def test_이름_기호_첨자_바꿈_전파(): def test_검사_까닭(): cases = { - "없는 이름": ("중간:환산거리", "환산계수 × 없는값", "모르는 이름"), + "없는 이름": ("중간:환산거리", "환산계수 × 없는값", "모르는 기호"), "괄호": ("중간:환산거리", "(환산계수 × L", "없음"), "아래 줄": ("중간:무게", "일운반회수 × 2", "아래 줄"), "순환": ("중간:무게", "무게 + 1", "자기 자신"), diff --git a/resources/tester/test_800_2_equation_layout.py b/resources/tester/test_800_2_equation_layout.py new file mode 100644 index 000000000..565c0f11e --- /dev/null +++ b/resources/tester/test_800_2_equation_layout.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""PLAN 800-2 · 800-3 — 수식 탭 변수 정의 영역 · 바로 고침 · 새 기호 연결 (서버 몫 · 임시 쓰기 없음).""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path[:0] = [str(ROOT), str(ROOT / "resources/master_data/scripts")] + +from M01_MasterData import M01_MasterData_Equation as eq # noqa: E402 +from M01_MasterData import M01_MasterData_Store as store # noqa: E402 + +KEY = "GC000211" +OFF = {"폼타이": "아님", "측압": "3t/㎡"} +ON = {"폼타이": "사용", "측압": "4t/㎡"} + + +def _defs(inputs): + return eq.read(KEY, inputs)["수식"]["정의"] + + +def _text(defs): + return json.dumps(defs, ensure_ascii=False) + + +def test_defs_merge_and_clean(): + defs = _defs(OFF) + signs = [(d["기호"], d["위"], d["아래"]) for d in defs] + assert len(signs) == len(set(signs)), "같은 변수는 한 번만" + assert not re.search(r"[A-Z]{2}\d{6}", _text(defs)), "DB 열쇠 글 0" + assert all(set(d) <= {"열쇠", "기호", "위", "아래", "이름", "갈래", "연결", "고침", "안씀"} for d in defs) + by = {d["기호"]: d for d in defs} + assert len(by["보통인부"]["연결"]) >= 3 # 중간 줄 + 표 둘 — 한 줄로 합침 + assert by["사용횟수"]["연결"] == ["입력 — 옵션에서 고름"] + + +def test_unpicked_branches_hidden(): + off, on = _text(_defs(OFF)), _text(_defs(ON)) + for col in ("4t/㎡", "5t/㎡", "6t/㎡", "폼타이 소요량"): + assert col not in off + assert "칸 4t/㎡" in on and "칸 3t/㎡" not in on and "칸 5t/㎡" not in on + assert "측압" in on and "측압" not in off # 폼타이 아님이면 측압 갈래도 안 탐 + + +def test_live_edit_preview_changes_total(): + spot = {"할일": "식", "자리": "호표:0", "식": "형틀목공 × 2"} + got = eq.preview(KEY, [spot], OFF) + assert got["되나"] and got["후"]["계"] > got["전"]["계"] + + +def test_unknown_symbol_asks_link_then_links(): + spot = {"할일": "식", "자리": "호표:0", "식": "형틀목공 × 2 + Z"} + got = eq.preview(KEY, [spot], OFF) + assert not got["되나"] and "모르는 기호 「Z」" in got["까닭"][0] + for link in ( + {"갈래": "상수", "값": "0"}, + {"갈래": "입력", "기본값": "0"}, + {"갈래": "표", "표": "QC000431", "칸": "형틀목공", "조건": {"유형": "유형"}}, + {"갈래": "요소", "요소": "LB000007"}, + ): + add = {"할일": "연결", "열쇠": "Z", "이름": "새값", **link} + got = eq.preview(KEY, [add, spot], OFF) + assert got["되나"], (link, got["까닭"]) + assert any(d["기호"] == "Z" for d in got["수식"]["정의"]) + + +def test_link_bad_input_blocked(): + bad = [ + {"갈래": "표", "표": "QC000431", "칸": "없는칸", "조건": {"유형": "유형"}}, + {"갈래": "표", "표": "QC000431", "칸": "형틀목공", "조건": {}}, + {"갈래": "요소", "요소": "GC000211"}, + {"갈래": "상수", "값": "abc"}, + ] + for link in bad: + got = eq.preview(KEY, [{"할일": "연결", "열쇠": "Z", **link}], OFF) + assert not got["되나"], link + + +def test_drop_guard_and_unused_input(): + got = eq.preview(KEY, [{"할일": "지움", "열쇠": "사용횟수"}], OFF) + assert not got["되나"] and "쓰는 곳" in got["까닭"][0] + add = {"할일": "연결", "열쇠": "Y", "갈래": "입력"} + got = eq.preview(KEY, [add], OFF) + unused = [d for d in got["수식"]["정의"] if d["기호"] == "Y"] + assert unused and unused[0].get("안씀") + assert eq.preview(KEY, [add, {"할일": "지움", "열쇠": "Y"}], OFF)["되나"] + + +def test_candidates(): + whole = store.loaded()[1] + got = eq.links({"갈래": "표", "검색": "합판거푸집 폼타이", "분류": ""}) + assert got["후보"] and "조건" in got["후보"][0] + assert eq.links({"갈래": "요소", "검색": "형틀목공", "분류": "인력"})["후보"] + assert eq.links({"갈래": "로직", "검색": "합판거푸집", "분류": ""})["후보"] + assert whole is not None