"""단가산출 다이어그램 입력 상자 판 — 고르는 규칙(`M02_DF_InputPanel_Build.ts`). 화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 순수 함수만 부름. 견본 줄 · 상자는 이 시험 안에 둠. """ import json import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "M02_MasterTemplete" / "DataFlow" / "M02_DF_InputPanel_Build.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 js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, () => ({})); const m = box.exports; const jobs = JSON.parse(fs.readFileSync(0, "utf8")); const out = jobs.map((j) => { if (j.op === "fn") return m[j.name](...j.args); let lg = j.lg; for (const [fn, ...args] of j.steps || []) lg = m[fn](lg, ...args); return lg; }); 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(SOURCE)], input=json.dumps(jobs, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(done.stdout) def logic() -> dict: plain = {"color": "#fff", "unit": "-", "x": 0, "y": 0} return { "key": None, "name": "견본", "resultUnit": "원/㎡", "userElements": [], "boxes": [ { "id": "b1", "kind": "input", "name": "입력", **plain, "input": {"mode": "variable", "elements": [], "variables": []}, }, {"id": "b2", "kind": "function", "name": "함수", **plain}, ], "links": [], } def call(name: str, *args): return run([{"op": "fn", "name": name, "args": list(args)}])[0] def edit(steps: list, lg: dict | None = None) -> dict: return run([{"op": "lg", "lg": lg or logic(), "steps": steps}])[0] LABOR = {"키": "LB000033", "이름": "석공", "규격": "", "단위": "원/일", "값": 268908} LABOR2 = {"키": "LB000002", "이름": "보통인부", "규격": "", "단위": "원/일", "값": 172068} MAT = { "키": "MT1", "이름": "각형흄관", "규격": "D=300mm", "단위": "M", "물가자료": 30000, "물가정보": 29600, } MACHINE = { "키": "EQ000001", "이름": "불도저", "규격": "7ton", "단위": {"가격": "천원", "손료계수": "10^-7/시간"}, "값": {"가격": 76547, "손료계수": 1811}, } RATE_ONE = {"키": "RT1", "이름": "산재보험료", "값": {"rate_percent": 3.56}} RATE_BRACKET = {"키": "RT2", "이름": "고용보험료", "값": {"brackets": []}} def test_요소는_이름과_규격으로_알약_이름이_됨(): labor = call("elementOf", "인력", LABOR) assert labor == {"ref": "LB000033", "name": "석공", "spec": "", "unit": "원/일"} got = call("elementOf", "재료", MAT) assert got["name"] == "각형흄관 D=300mm" and got["unit"] == "M" def test_기계는_값_칸을_골라_요소가_됨(): assert call("machineFields", MACHINE) == ["가격", "손료계수"] got = call("elementOf", "기계", MACHINE, "손료계수") assert got["ref"] == "EQ000001#손료계수" and got["unit"] == "10^-7/시간" assert got["name"].endswith("· 손료계수") assert call("queryOf", "기계", "EQ000001#손료계수") == { "group": "기계", "ref": "EQ000001", "field": "손료계수", } def test_값_읽기_재료는_낮은_값_요율은_비율(): assert call("valueOf", "재료", MAT) == 29600 assert call("valueOf", "요율", RATE_ONE) == 3.56 assert call("valueOf", "요율", RATE_BRACKET) is None assert call("elementOf", "요율", RATE_ONE)["unit"] == "%" assert call("elementOf", "요율", RATE_BRACKET)["unit"] == "-" assert call("valueOf", "기계", MACHINE, "가격") == 76547 def test_사용자_요소는_그_그룹의_ref_로_물음(): assert call("queryOf", "인력", "UE-0001") == {"group": "인력", "ref": "UE-0001"} assert call("queryOf", "재료", "MT1") == {"group": "재료", "ref": "MT1"} row = {"키": "UE-0001", "이름": "특수공", "규격": "", "단위": "원/일", "값": 300000} assert call("elementOf", "인력", row)["unit"] == "원/일" assert call("elementOf", "요율", {**row, "단위": "%"})["unit"] == "%" def test_상자_단위는_같으면_그_단위_여럿이면_하이픈(): one = edit([["setKind", "b1", "인력"], ["addElement", "b1", call("elementOf", "인력", LABOR)]]) assert one["boxes"][0]["unit"] == "원/일" two = edit([["addElement", "b1", call("elementOf", "인력", LABOR2)]], one) assert two["boxes"][0]["unit"] == "원/일" and len(two["boxes"][0]["input"]["elements"]) == 2 oil = {"ref": "X", "name": "경유", "spec": "", "unit": "원/L"} assert edit([["addElement", "b1", oil]], two)["boxes"][0]["unit"] == "-" def test_같은_요소_이름은_한_번만(): el = call("elementOf", "인력", LABOR) lg = edit( [ ["setKind", "b1", "인력"], ["addElement", "b1", el], ["addElement", "b1", el], ["addElement", "b1", {**el, "ref": "다른"}], ] ) assert len(lg["boxes"][0]["input"]["elements"]) == 1 def test_요소를_빼면_그_알약에서_나간_선도_지움(): el1, el2 = call("elementOf", "인력", LABOR), call("elementOf", "인력", LABOR2) base = edit([["setKind", "b1", "인력"], ["addElement", "b1", el1], ["addElement", "b1", el2]]) base["links"] = [ {"id": "l1", "from": {"box": "b1", "name": "석공"}, "to": {"box": "b2", "name": "a"}}, {"id": "l2", "from": {"box": "b1", "name": "보통인부"}, "to": {"box": "b2", "name": "b"}}, ] out = edit([["removeElement", "b1", "LB000033"]], base) assert [x["id"] for x in out["links"]] == ["l2"] assert [e["name"] for e in out["boxes"][0]["input"]["elements"]] == ["보통인부"] def test_갈래를_바꾸면_고른_것을_비움_표는_요소와_함께_못_둠(): el = call("elementOf", "인력", LABOR) lg = edit([["setKind", "b1", "인력"], ["addElement", "b1", el], ["setKind", "b1", "재료"]]) assert lg["boxes"][0]["input"]["group"] == "재료" assert lg["boxes"][0]["input"]["elements"] == [] t = edit([["setKind", "b1", "소요량"], ["setTable", "b1", {"key": "QF000421"}, "메쌓기(인력)"]]) assert t["boxes"][0]["input"]["table"] == {"key": "QF000421"} assert t["boxes"][0]["name"] == "메쌓기(인력)" and t["boxes"][0]["unit"] == "-" row = edit([["setTable", "b1", {"key": "QF000421", "row": 3}]], t) assert row["boxes"][0]["input"]["table"] == {"key": "QF000421", "row": 3} assert call("pillNames", row["boxes"][0]) == ["표"] def test_변수_넣기_고치기_삭제_알약_이름(): v = {"name": "면적", "type": "number", "unit": "㎡", "example": 100, "range": [1, 1000]} lg = edit([["setKind", "b1", "변수"], ["putVariable", "b1", v]]) box = lg["boxes"][0] assert box["input"]["mode"] == "variable" and box["unit"] == "㎡" assert call("pillNames", box) == ["면적"] renamed = edit([["putVariable", "b1", {**v, "name": "연장", "unit": "m"}, "면적"]], lg) assert call("pillNames", renamed["boxes"][0]) == ["연장"] and renamed["boxes"][0]["unit"] == "m" gone = edit([["removeVariable", "b1", "연장"]], renamed) assert gone["boxes"][0]["input"]["variables"] == [] and gone["boxes"][0]["unit"] == "-" def test_변수_검사는_쉬운_말(): ok = {"name": "면적", "type": "number", "unit": "㎡", "example": 100, "range": [1, 1000]} assert call("checkVariable", ok, []) is None assert call("checkVariable", {**ok, "name": " "}, []) == "이름을 적어 주세요" assert call("checkVariable", ok, [ok]) == "같은 이름의 변수가 이미 있음" assert call("checkVariable", {**ok, "range": [9, 1]}, []) == "범위는 아래가 위보다 작아야 함" assert call("checkVariable", {**ok, "example": 5000}, []) == "예시값이 범위 밖" many = {**ok, "type": "choice", "choices": ["가"]} assert call("checkVariable", many, []) == "선택 목록은 둘 이상" bad = {**ok, "type": "choice", "choices": ["가", "나"], "example": "다"} assert call("checkVariable", bad, []) == "예시값이 선택 목록에 없음" def test_환율_유가전력_알약_이름에_날짜가_안_들어감(): fx = {"키": "FX1", "이름": "USD", "규격": "2026-08-14", "단위": "원/USD", "값": 1414.9} assert call("elementOf", "환율", fx)["name"] == "USD" oil = { "키": "MO1", "이름": "휘발유", "규격": "2026-08-14", "상세구분": "서울", "단위": "원/L", "값": 1900, } assert call("elementOf", "유가전력", oil)["name"] == "휘발유 서울" def test_단위가_안_맞게_저장된_상자는_열_때_맞춤(): el1, el2 = call("elementOf", "인력", LABOR), call("elementOf", "인력", LABOR2) lg = logic() lg["boxes"][0]["input"] = { "mode": "group", "group": "인력", "elements": [el1, el2], "variables": [], } assert lg["boxes"][0]["unit"] == "-" fixed = run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0] assert fixed["boxes"][0]["unit"] == "원/일" again = run([{"op": "fn", "name": "reconcileUnit", "args": [fixed, "b1"]}])[0] assert again == fixed oil = {"ref": "X", "name": "경유", "spec": "", "unit": "원/L"} lg["boxes"][0]["input"]["elements"].append(oil) lg["boxes"][0]["unit"] = "원/일" assert ( run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0]["boxes"][0]["unit"] == "-" ) one = { "mode": "variable", "elements": [], "variables": [{"name": "면적", "type": "number", "unit": "㎡", "example": 1}], } lg["boxes"][0]["input"] = one assert ( run([{"op": "fn", "name": "reconcileUnit", "args": [lg, "b1"]}])[0]["boxes"][0]["unit"] == "㎡" )