"""단가산출 흐름도 「식 보기」(`M02_DF_FormulaView.ts`) · 엑셀 「조건 만들기」 글 쪽(`spreadsheet_condition_text.ts`). 화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 상대 경로 TS(엔진 포함)는 이어 트랜스파일 · 화면 부품(@ui 의 elements · locale · modal)은 빈 흉내 · 엔진이 쓰는 @ui/sheet 는 그대로. 견본 함수 상자는 이 시험 안에 둠. """ import json import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] VIEW = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_FormulaView.ts" TEXT = ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_condition_text.ts" RUNNER = r""" const fs = require("fs"); const path = require("path"); const [root, viewFile, textFile] = process.argv.slice(1); const ts = require(path.join(root, "config", "node_modules", "typescript")); const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : () => undefined) }); const cache = {}; function load(f) { if (cache[f]) return cache[f].exports; const js = ts.transpileModule(fs.readFileSync(f, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = (cache[f] = { exports: {} }); const req = (id) => { if (id.startsWith(".") || id.startsWith("@ui/sheet/")) { const p = id.startsWith("@ui/sheet/") ? path.join(root, "ui_template", "sheet", id.slice("@ui/sheet/".length)) : path.resolve(path.dirname(f), id); return fs.existsSync(p + ".ts") ? load(p + ".ts") : stub; } return stub; }; new Function("exports", "module", "require", js)(box.exports, box, req); return box.exports; } const v = load(viewFile); const t = load(textFile); const jobs = JSON.parse(fs.readFileSync(0, "utf8")); const out = jobs.map((job) => { if (job.op === "lines") { const values = v.cellValuesOf(job.box, job.arrived); return { values, lines: v.formulaLines(job.box, values) }; } if (job.op === "toFormula") return t.draftToFormula(job.draft); if (job.op === "toDraft") return t.formulaToDraft(job.formula); return null; }); process.stdout.write(JSON.stringify(out)); """ def run(jobs: list[dict]) -> list: if shutil.which("node") is None: pytest.skip("node 가 없음") done = subprocess.run( ["node", "-e", RUNNER, str(ROOT), str(VIEW), str(TEXT)], input=json.dumps(jobs, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(done.stdout) def func_box(cells: dict, inputs: list, variables: list, out_cell: str, out_name: str) -> dict: return { "id": "f1", "kind": "function", "name": "석공 품", "color": "", "unit": "인", "x": 0, "y": 0, "func": { "sheet": {"id": "s1", "이름": "석공 품", "칸": cells}, "inputs": inputs, "variables": variables, "output": {"cell": out_cell, "name": out_name, "unit": "인"}, }, } def var(name: str, cell: str, example) -> dict: return {"name": name, "type": "number", "unit": "-", "example": example, "cell": cell} # 석공량(받는 알약 B1) · 높이(변수 B2) → B3 = 석공량 × (1 + 높이 ÷ 100) PLAIN = func_box( { "A1": {"값": "석공량"}, "B3": {"식": "B1*(1+B2/100)"}, }, [{"port": "석공량", "cell": "B1"}], [var("높이", "B2", 4)], "B3", "석공 인", ) def test_이름_식과_값_식(): (got,) = run([{"op": "lines", "box": PLAIN, "arrived": {"석공량": 0.15}}]) assert got["values"]["B3"] == pytest.approx(0.156) (line,) = got["lines"] assert line["name"] == "석공 인" assert line["named"] == "석공량 × (1 + 높이 ÷ 100)" assert line["valued"] == "0.15 × (1 + 4 ÷ 100) = 0.156" assert "branches" not in line def test_중간_칸이_먼저_결과_칸이_마지막(): box = func_box( {"B3": {"식": "B1*B2"}, "B4": {"식": "B3+1"}}, [{"port": "석공량", "cell": "B1"}], [var("높이", "B2", 4)], "B4", "합", ) (got,) = run([{"op": "lines", "box": box, "arrived": {"석공량": 2}}]) assert [x["name"] for x in got["lines"]] == ["B3", "합"] # 연결 없는 칸은 주소 그대로 assert got["lines"][1]["named"] == "B3 + 1" assert got["lines"][1]["valued"] == "8 + 1 = 9" @pytest.mark.parametrize( ("ground", "now"), [("토사", [True, False]), ("암", [False, True])], ) def test_IF_줄과_지금_갈래(ground, now): box = func_box( {"B3": {"식": 'IF(B4="토사",B1*2,B1*3)'}}, [{"port": "석공량", "cell": "B1"}], [{"name": "지반", "type": "choice", "unit": "-", "example": ground, "cell": "B4"}], "B3", "결과", ) (got,) = run([{"op": "lines", "box": box, "arrived": {"석공량": 10}}]) (line,) = got["lines"] assert [b["now"] for b in line["branches"]] == now assert line["branches"][0]["when"] == '지반 = "토사"' assert line["branches"][0]["then"] == "석공량 × 2" assert line["branches"][1] == {"when": "아니면", "then": "석공량 × 3", "now": now[1]} assert line["valued"].endswith("= 20" if ground == "토사" else "= 30") def test_조건_만들기_식과_다시_줄(): draft = { "branches": [ { "left": "B1", "op": "=", "right": {"kind": "value", "value": "토사"}, "then": {"kind": "cell", "cell": "C1"}, }, { "left": "B1", "op": "≥", "right": {"kind": "value", "value": 3}, "then": {"kind": "value", "value": 0.5}, }, ], "otherwise": {"kind": "value", "value": 0}, } (formula,) = run([{"op": "toFormula", "draft": draft}]) assert formula == 'IF(B1="토사",C1,IF(B1>=3,0.5,0))' (back,) = run([{"op": "toDraft", "formula": formula}]) assert back == draft @pytest.mark.parametrize("formula", ["SUM(B1:B2)", "IF(B1+1>2,1,2)", "IF(B1>2,B2*2,0)"]) def test_못_바꾸는_식은_null(formula): (back,) = run([{"op": "toDraft", "formula": formula}]) assert back is None