"""단가산출 흐름도 입력 판 — 다른 로직 입력 = 마스터 로직 고르기(순수 부분 · `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; (async () => { const jobs = JSON.parse(fs.readFileSync(0, "utf8")); const out = []; for (const j of jobs) { if (j.name === "wouldCycle") { const graph = j.args[2]; out.push(await m.wouldCycle(j.args[0], j.args[1], async (k) => graph[k] ?? null)); } else out.push(await m[j.name](...j.args)); } process.stdout.write(JSON.stringify(out)); })(); """ def call(name: str, *args): if shutil.which("node") is None: pytest.skip("node 가 없음") done = subprocess.run( ["node", "-e", RUNNER, str(ROOT), str(SOURCE)], input=json.dumps([{"name": name, "args": list(args)}], ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(done.stdout)[0] def row(key, name, book, chapter, no="", unit="원/㎡", **more): return { "file": f"로직_{book}.json", "구분": book, "상세구분": chapter, "키": key, "원문번호": no, "이름": name, "결과단위": unit, **more, } ITEMS = [ row("GF000219", "돌쌓기 메쌓기(인력)", "산림품셈", "13장 구조물", "13-4-1"), row("GF000220", "돌쌓기 찰쌓기(인력)", "산림품셈", "13장 구조물", "13-4-2"), row("GF000025", "떼붙임(재배잔디)", "산림품셈", "05장 녹화", "5-12"), row("GC000051", "보행자 안전통로", "건설품셈", "01장 공통", "1-3-9", "원/m"), row("DF-0001", "내 흄관 로직", "흐름도", "내가 만든 것", "", "원/m"), ] def keys(items): return [i["키"] for i in items] def test_찾기는_띄어쓰기로_나눈_말을_모두_포함(): got = call("filterLogics", ITEMS, {"text": "돌쌓기 메쌓기"}) assert keys(got) == ["GF000219"] assert keys(call("filterLogics", ITEMS, {"text": "돌쌓기"})) == ["GF000219", "GF000220"] assert keys(call("filterLogics", ITEMS, {"text": "돌 쌓기메쌓기"})) == [ "GF000219" ] # 띄어쓰기는 가리지 않음 assert keys(call("filterLogics", ITEMS, {"text": "13-4"})) == [ "GF000219", "GF000220", ] # 원문번호 assert keys(call("filterLogics", ITEMS, {"text": "gc000051"})) == [ "GC000051" ] # 번호 · 대소문자 assert call("filterLogics", ITEMS, {"text": ""}) == ITEMS def test_원문_장으로_거름(): assert keys(call("filterLogics", ITEMS, {"text": "", "book": "건설품셈"})) == ["GC000051"] got = call("filterLogics", ITEMS, {"text": "", "book": "산림품셈", "chapter": "05장 녹화"}) assert keys(got) == ["GF000025"] assert call("filterLogics", ITEMS, {"text": "메쌓기", "book": "건설품셈"}) == [] def test_원문_장_목록은_나온_차례(): assert call("logicBooks", ITEMS) == ["산림품셈", "건설품셈", "흐름도"] assert call("logicChapters", ITEMS, "산림품셈") == ["13장 구조물", "05장 녹화"] assert call("logicChapters", ITEMS, "") == [ "13장 구조물", "05장 녹화", "01장 공통", "내가 만든 것", ] def box(id_, kind, name, **more): return { "id": id_, "kind": kind, "name": name, "color": "#fff", "unit": "-", "x": 0, "y": 0, **more, } FLOW = { "key": "GF000219", "name": "돌쌓기 메쌓기(인력)", "resultUnit": "원/㎡", "userElements": [], "links": [], "boxes": [ box( "b1", "input", "입력", input={ "mode": "variable", "elements": [], "variables": [ {"name": "뒷길이", "type": "number", "unit": "㎝", "example": 30}, { "name": "돌", "type": "choice", "unit": "", "choices": ["깬돌", "견치돌"], "example": "깬돌", }, ], }, ), box("o1", "output", "노무비", unit="원/㎡", output={"cost": "노무비", "unit": "원/㎡"}), ], } def test_흐름_로직_고르기는_출력_상자와_설계_때_정할_것(): ref = call("masterRef", "GF000219", FLOW, False, "원/㎡") assert ref["key"] == "GF000219" assert ref["outputs"] == [{"name": "노무비", "unit": "원/㎡"}] assert ref["designs"] == [{"name": "뒷길이", "unit": "㎝"}, {"name": "돌", "unit": "-"}] assert ref["examples"] == {"뒷길이": 30, "돌": "깬돌"} def test_옛_로직_변환은_노무비_재료비_경비_계_넷(): ref = call("masterRef", "GF000219", FLOW, True, "원/㎡") assert [o["name"] for o in ref["outputs"]] == ["노무비", "재료비", "경비", "계"] assert {o["unit"] for o in ref["outputs"]} == {"원/㎡"} assert [d["name"] for d in ref["designs"]] == ["뒷길이", "돌"] # 받는 알약은 흐름 그대로 def test_이어지지_않은_다른_로직_받는_알약도_설계_때_정할_것(): inner = { "mode": "logic", "elements": [], "variables": [], "logic": { "key": "GF000250", "outputs": [{"name": "작업량", "unit": "㎥/시간"}], "designs": [{"name": "지역", "unit": "-"}, {"name": "기계", "unit": "-"}], "examples": {"지역": "서울"}, }, } flow = {**FLOW, "boxes": [*FLOW["boxes"], box("b2", "input", "작업량", input=inner)]} flow["links"] = [ {"id": "l1", "from": {"box": "b1", "name": "돌"}, "to": {"box": "b2", "name": "기계"}} ] ref = call("logicRefOf", "X", flow) names = [d["name"] for d in ref["designs"]] assert "지역" in names and "기계" not in names # 선이 이어진 받는 알약은 뺌 assert ref["examples"]["지역"] == "서울" def test_자기_자신과_돌고_도는_로직은_막음(): graph = { "A": { "boxes": [ box( "b", "input", "x", input={"mode": "logic", "elements": [], "variables": [], "logic": {"key": "B"}}, ) ] }, "B": { "boxes": [ box( "b", "input", "x", input={ "mode": "logic", "elements": [], "variables": [], "logic": {"key": "SELF"}, }, ) ] }, "C": {"boxes": []}, } assert call("wouldCycle", "SELF", "SELF", graph) is True # 자기 자신 assert call("wouldCycle", "SELF", "A", graph) is True # A → B → SELF assert call("wouldCycle", "SELF", "C", graph) is False assert call("wouldCycle", None, "A", graph) is False # 새 로직은 아직 키가 없어 돌 수 없음 assert call("wouldCycle", "SELF", "없음", graph) is False # 못 읽으면 막지 않음