"""흐름 로직 왼쪽 「단가산출 상세」 줄 만들기(`M01_MasterData_UI_LogicLab_FlowRows.ts`) — 순수 부분. 화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 상대 경로 TS(DataFlow Round …)는 이어 트랜스파일 · 화면 부품(@ui · @config)은 빈 흉내. 견본 로직 · 계산 결과는 이 시험 안에 둠(마스터 내용에 기대지 않음). """ import json import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_FlowRows.ts" RUNNER = r""" const fs = require("fs"); const path = require("path"); const [root, file] = process.argv.slice(1); const ts = require(path.join(root, "config", "node_modules", "typescript")); const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : k === "API_BASE_URL" ? "http://127.0.0.1:1/api" : () => 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(".")) { const p = 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 m = load(file); const jobs = JSON.parse(fs.readFileSync(0, "utf8")); const out = jobs.map((job) => m.flowRows(job.lg, job.ev, job.round)); process.stdout.write(JSON.stringify(out)); setTimeout(() => process.exit(0), 50); """ ROUND = {"item": {"places": 1, "method": "버림"}, "total": {"places": 0, "method": "버림"}} def box(id_, kind, name, unit="원", **extra): return { "id": id_, "kind": kind, "name": name, "color": "#000", "unit": unit, "x": 0, "y": 0, **extra, } def out_box(id_, name, cost, unit="원", round_=None): output = {"cost": cost, "unit": unit} if round_: output["round"] = round_ return box(id_, "output", name, unit, output=output) def func_box(id_, name, out_name, unit="시간"): func = { "sheet": {"id": "s-" + id_, "이름": name, "칸": {}}, "inputs": [], "variables": [], "output": {"cell": "B1", "name": out_name, "unit": unit}, } return box(id_, "function", name, unit, func=func) def logic(boxes): return { "key": None, "name": "시험", "resultUnit": "원/㎡", "boxes": boxes, "links": [], "userElements": [], } def result(outputs, cost, ports=None, errors=None): return {"ports": ports or {}, "outputs": outputs, "cost": cost, "errors": errors or []} def run(jobs: list[dict]) -> list: node = "node" done = subprocess.run( [node, "-e", RUNNER, str(ROOT), str(SOURCE)], input=json.dumps(jobs), capture_output=True, text=True, timeout=60, check=False, ) assert done.returncode == 0, done.stderr return json.loads(done.stdout) @pytest.fixture(scope="module") def sample(): lg = logic( [ out_box("o1", "인건비", "노무비"), out_box("o2", "재료값", "재료비"), out_box("o3", "시간당 작업량", "값", "㎥/시간"), out_box("o4", "특별 끝수", "경비", round_={"places": 2, "method": "반올림"}), func_box("f1", "운반 시간", "시간", "시간"), ] ) ev = result( {"o1": 3216.69, "o2": 100.04, "o3": 12.5, "o4": 1.239}, { "노무비": 3216.69, "재료비": 100.04, "경비": 1.239, "값들": {"시간당 작업량": 12.5}, "계": 3317.969, }, ports={"f1": {"시간": 6.024}}, ) return run([{"lg": lg, "ev": ev, "round": ROUND}])[0] def group(rows, key): return next(g for g in rows["groups"] if g["key"] == key) def test_group_order_and_costs_only(sample): assert [g["key"] for g in sample["groups"]] == ["labor", "material", "cost", "plain"] def test_line_uses_output_round(sample): # 출력 끝수를 안 정한 비용 줄 = 기본 0.1원 버림 → 3,216.6 (정수로 반올림하지 않음) assert group(sample, "labor")["lines"][0]["text"] == "3,216.6" assert group(sample, "material")["lines"][0]["text"] == "100" # 그 출력의 끝수가 있으면 그 자리대로(소수 둘째 반올림) assert group(sample, "cost")["lines"][0]["text"] == "1.24" def test_plain_value_is_not_rounded_and_has_no_subtotal(sample): plain = group(sample, "plain") assert plain["lines"][0]["text"] == "12.5" assert plain["lines"][0]["unit"] == "㎥/시간" assert plain["sub"] is None def test_subtotal_and_total_floor_to_won(sample): assert group(sample, "labor")["sub"] == 3216 assert group(sample, "labor")["subText"] == "3,216" assert sample["total"] == 3317 assert sample["totalText"] == "3,317" def test_function_rows(sample): assert sample["funcs"] == [ {"box": "f1", "name": "운반 시간", "unit": "시간", "value": 6.024, "text": "6.024"} ] def test_empty_group_is_hidden(): lg = logic([out_box("o1", "인건비", "노무비")]) ev = result({"o1": 10}, {"노무비": 10, "재료비": 0, "경비": 0, "값들": {}, "계": 10}) rows = run([{"lg": lg, "ev": ev, "round": ROUND}])[0] assert [g["key"] for g in rows["groups"]] == ["labor"] assert rows["total"] == 10 def test_missing_value_shows_dash(): lg = logic([out_box("o1", "인건비", "노무비")]) ev = result( {"o1": None}, {"노무비": 0, "재료비": 0, "경비": 0, "값들": {}, "계": 0}, errors=["끊김"] ) rows = run([{"lg": lg, "ev": ev, "round": ROUND}])[0] assert group(rows, "labor")["lines"][0]["text"] == "—" assert rows["errors"] == ["끊김"]