revert(802-1): 801-1 wip 옛 나무 위 고침 걷음 — 요구 664 수식 문서로 다시 짬

- 걷음: 43784a4c0 의 옛 수식 나무 위 고침(서버 · 화면 · css) = 그 앞 판으로 되돌림
- 남김: master_equation_sign.py · ui_template_equation_text.ts · ui_template_equation_edit.ts(다시 쓸 조각 · 지금 안 부름) · text 는 `입력글` 을 제 타입에
- 지움: test_801_1_equation_inline.py — 걷은 옛 나무 고침을 재는 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 17:55:58 +09:00
co-authored by Claude Opus 5.5
parent 6c7618c54a
commit 2376e70fbb
11 changed files with 100 additions and 456 deletions
@@ -28,7 +28,6 @@ from pathlib import Path
import master_auto as ma
import master_cut as mcut
import master_equation_sign as msign
import master_equation_view as view
import master_formula as mf
import master_material as mm
@@ -346,8 +345,7 @@ def values(tree: dict) -> dict:
if kind in ("변수", "블록"):
value = tree.get("값")
got = _num(value) if value is not None else {"꼴": "글", "글": "?"}
unit = tree.get("단위") or tree.get("결과단위") or "" # 값 식 숫자 단위(801-4)
return {**got, "열쇠": tree.get("열쇠"), "갈래": tree.get("갈래") or "블록", "단위": unit}
return {**got, "열쇠": tree.get("열쇠"), "갈래": tree.get("갈래") or "블록"}
if kind == "조건":
if tree.get("걸림"):
return values(tree["참" if tree["걸림"] == 1 else "거짓"])
@@ -464,7 +462,7 @@ 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)}
out = msym.apply( # 기호 먼저(800-1) → 한 마디 줄 · 로직 부름 기호(801) → 정의(800-2)
out = msym.apply( # 기호 먼저(800-1) → 정의는 기호 씌운 줄로(800-2)
{
"열쇠": row.get("키"),
"이름": row.get("이름") or "",
@@ -476,7 +474,6 @@ def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
**sub,
}
)
msign.settle(out)
if depth == 0:
out["정의"] = view.definitions(out_lines, row, out["변수표"], _unused(row))
return _clean(out)
@@ -414,13 +414,7 @@ def apply(master, row: dict, edits: list, words=None) -> tuple[dict, list[str],
_show(new, key, ed, why)
elif job == "연결":
add, bad = link.plan(master, ed)
mid = key in {str(s.get("이름")) for s in new.get("중간") or []}
if add and mid and add.get("식"): # 있는 계산 변수 연결 바꿈(801) = 그 줄 식
_set_formula(new, {"자리": f"중간:{key}", "식": add["식"]}, why, spots, words)
elif add and mid:
why.append(f"「{key}」 계산 변수는 입력으로 못 바꿈 — 표 · 요소 · 로직 · 상수")
else:
_add(new, add, why, spots, words) if add else why.append(bad)
_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:
@@ -76,8 +76,8 @@ def definitions(lines: list[dict], row: dict, rows: list[dict], unused) -> list[
leaf[str(line.get("열쇠"))] = {"갈래": "상수", "글": tree.get("글")}
elif tree.get("갈래") in ("찾기", "요소") or tree.get("꼴") == "블록": # 한 마디 식
leaf[str(line.get("열쇠"))] = tree
found = ([head] if head and line.get("갈래") in ("중간", "결과") else []) + (
[] if line.get("한마디") else visible(tree, []) # 한 마디 줄 = 줄 변수 하나(801)
found = ([head] if head and line.get("갈래") in ("중간", "결과") else []) + visible(
tree, []
)
for node in found:
key = str(node.get("열쇠") or "")
@@ -97,7 +97,7 @@ def definitions(lines: list[dict], row: dict, rows: list[dict], unused) -> list[
if kind == "블록" and not leaf.get(key):
link = "로직 부르기" # 이름이 곧 그 로직 — 되풀이 안 함
name = (look.get("이름") or key) if mine else look.get("이름") or ""
name = node.get("칸") or name if kind == "찾기" and not node.get("칸글") else name
name = node.get("칸") or name if kind == "찾기" else name
one = {
"열쇠": key if mine else "",
"기호": look.get("기호") or name, # 줄 참조도 씌운 기호(master_symbol 한 곳)
@@ -1,169 +0,0 @@
# -*- coding: utf-8 -*-
"""PLAN 801-1 · 801-2 · 801-4 — 수식 = 발표 자료 꼴 · 보이는 식 그 자리 고침 (서버 · 글 꼴 TS 를 node 로).
한 마디 줄 「q = 0.7」(표 칸 글 0) · 로직 부름 = 기호 · 반올림 감싸개 · 안 탄 갈래 안 보임 ·
값 식 숫자 단위 · 나무 → 키보드 식(`ui_template_equation_text.ts`) 왕복 = 금액 같음 ·
값 고침 = 그 변수 값 · 기호 고침(연결) = 그 줄 식.
"""
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
from fastapi.encoders import decimal_encoder
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
import master_formula as mf # noqa: E402
TEXT = ROOT / "ui_template/ui_template_equation_text.ts"
MODEL = ROOT / "M01_MasterData/logic/M01_Logic_Equation_Model.ts"
PLAIN = ("적용", "값", "기준")
KEYS = ("GF000098", "GF000099", "GC000211", "GC001148", "GC000996", "GC000998", "GC000999")
OFF = {"폼타이": "아님", "측압": "3t/㎡"}
needs_node = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
def _read(key, inputs=None):
return eq.read(key, inputs or {})["수식"]
def _ts(call: str, data) -> object:
"""글 꼴 TS(+ 모델 `valueEdit`)를 node 로 — import 줄 걷고 이어 붙임."""
src = "\n".join(
re.sub(r"^import[^;]*;\n", "", p.read_text(encoding="utf-8"), flags=re.M)
for p in (TEXT, MODEL)
)
script = f"{src}\nconst DATA = {json.dumps(data, ensure_ascii=False, default=decimal_encoder)};\nconsole.log(JSON.stringify({call}));"
got = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript"],
input=script, capture_output=True, text=True, encoding="utf-8", timeout=120,
) # fmt: skip
assert got.returncode == 0, got.stderr[-2000:]
return json.loads(got.stdout)
def _sign(n):
return (n.get("기호"), n.get("아래") or "", n.get("위") or "")
def _nodes(tree, out):
if isinstance(tree, dict):
if tree.get("꼴") in ("변수", "블록"):
out.append(tree)
for k, v in tree.items():
if k != "나무":
_nodes(v, out)
elif isinstance(tree, list):
for v in tree:
_nodes(v, out)
return out
def test_한마디_줄_표_칸_글_0():
lines = {x["열쇠"]: x for x in _read("GF000098")["줄"]}
for key in ("q", "K", "f", "E", "Cm"):
line = lines[key]
assert line["한마디"] and _sign(line["이름식"]) == _sign(line["결과"])
assert "적용" not in json.dumps(_sign(line["이름식"]), ensure_ascii=False)
defs = _read("GF000098")["정의"]
assert [d["기호"] for d in defs] == ["q", "K", "f", "E", "Cm", "Q"] # 표 찾기 따로 줄 없음
def test_로직_부름_기호_뜻없는_칸_글_0():
got = _read("GF000099")
lines = {x["열쇠"]: x for x in got["줄"]}
assert lines["작업량"]["한마디"] and lines["작업량"]["이름식"]["꼴"] == "블록"
assert _sign(lines["작업량"]["이름식"]) == _sign(lines["작업량"]["결과"])
for key in KEYS:
for node in _nodes([x.get("이름식") for x in _read(key, OFF)["줄"]], []):
text = "".join(str(x) for x in _sign(node))
assert not re.search(r"[가-힣]", node.get("기호") or ""), (key, node.get("기호"))
if node.get("갈래") == "찾기":
assert not any(w in text for w in PLAIN), (key, text)
def test_값_식_단위():
res = next(x for x in _read("GF000098")["줄"] if x["갈래"] == "결과")
units = [n.get("단위") for n in _nodes(res["값식"], []) + [res["값식"]]]
flat = json.dumps(res["값식"], ensure_ascii=False, default=str)
assert '"단위": "초"' in flat or "초" in units
@needs_node
def test_반올림_감싸개_안_탄_갈래_안_보임():
res = next(x for x in _read("GF000098")["줄"] if x["갈래"] == "결과")
assert res["이름식"]["꼴"] == "함수" and res["이름식"]["이름"] == "반올림" # 셈 그대로
leaves = _ts("leavesOf(DATA).map((n) => n.글 ?? n.기호)", res["값식"])
assert leaves[-1] != "3" and len(leaves) == 6 # 3,600 · q · K · f · E · Cm — 자리 3 없음
line = next(x for x in _read("GC000211", OFF)["줄"] if x["열쇠"].startswith("폼타이"))
shown = _ts("leavesOf(DATA).map((n) => n.글 ?? n.기호)", line["이름식"])
assert "12.7㎜" not in json.dumps(shown, ensure_ascii=False) # 안 탄 만약 조건 · 갈래 없음
@needs_node
def test_나무_글_왕복_금액_같음():
for key in KEYS:
got = _read(key, OFF)
lines = [x for x in got["줄"] if x.get("이름식")]
texts = _ts(
"DATA.map((l) => textOf(l.갈래 === '호표' ? l.이름식.우 : l.이름식))",
lines,
)
edits = [
{"할일": "식", "자리": _spot(line), "식": text} for line, text in zip(lines, texts)
]
back = eq.preview(key, edits, OFF)
assert back["되나"], (key, back["까닭"])
# 같은 엔진 식(마디 나무) = 같은 셈 — 금액은 고친 중간 줄 표시 자리(요구 658)가 붙어 따로 안 봄
old = store.loaded()[1].logic(key)
for part, field in (("중간", "식"), ("호표", "수량"), ("덧줄", "식")):
for a, b in zip(old.get(part) or [], back["줄"].get(part) or []):
assert mf.parse(str(a[field])) == mf.parse(str(b[field])), (key, a, b)
if "결과" in old:
assert mf.parse(old["결과"]["식"]) == mf.parse(back["줄"]["결과"]["식"])
def _spot(line):
kind = line["갈래"]
if kind in ("호표", "덧줄"):
return f"{kind}:{line.get('차례', 0)}"
return "결과" if kind == "결과" else f"중간:{line['열쇠']}"
@needs_node
def test_값_고침_그_변수_값():
lines = _read("GF000098")["줄"]
q = next(x for x in lines if x["열쇠"] == "q")
res = next(x for x in lines if x["갈래"] == "결과")
got = _ts("valueEdit(DATA.q, DATA.q.결과, '0.8')", {"q": q})
assert got == {"edits": [{"할일": "식", "자리": "중간:q", "식": "0.8"}]}
after = eq.preview("GF000098", got["edits"], {})
assert float(after["후"]["결과"]) == pytest.approx(66.528)
lit = _ts(
"valueEdit(DATA, leavesOf(DATA.이름식)[0], '3601').edits[0]",
res,
) # 결과 식의 3,600 → 3,601 · 반올림 감싸개 그대로
assert lit["자리"] == "결과" and lit["식"].startswith("반올림(3601 ")
after = eq.preview("GF000098", [lit], {})
assert after["되나"] and after["후"]["결과"] != after["전"]["결과"]
def test_기호_고침_연결_바꿈():
ed = {"할일": "연결", "열쇠": "q", "갈래": "상수", "값": "0.8"}
got = eq.preview("GF000098", [ed], {})
assert got["되나"] and float(got["후"]["결과"]) == pytest.approx(66.528)
q = next(d for d in got["수식"]["정의"] if d["기호"] == "q")
assert q["연결"] == ["상수 — 0.8"]
bad = eq.preview("GF000098", [{"할일": "연결", "열쇠": "q", "갈래": "입력"}], {})
assert not bad["되나"]