"""단가산출 흐름도 입력 판 — 다른 로직 입력 = 마스터 로직 고르기(순수 부분 · `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 if (j.name === "fillTemplate") { const vals = j.args[2]; out.push(m.fillTemplate(j.args[0], j.args[1], (p) => vals[`${p.box}.${p.name}`])); } 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 # 못 읽으면 막지 않음 def test_조건_찾기는_인력_기계_유가전력에_쓰고_이름_또는_키가_같은_줄(): labor = {"키": "LB000033", "원문번호": "1033", "이름": "석공"} assert call("matchesRow", labor, "이름", "석공") is True assert call("matchesRow", labor, "이름", "석 공") is True # 띄어쓰기는 가리지 않음 assert call("matchesRow", labor, "이름", "할석공") is False # 같음이라 일부만 맞아선 안 잡음 assert call("matchesRow", labor, "키", "lb000033") is True assert call("matchesRow", labor, "키", "1033") is True # 원문번호도 키로 침 assert call("matchesRow", labor, "이름", "") is False # 알약 값이 비면 안 잡음 def test_조건_찾기_요소와_검사(): port = {"box": "b2", "name": "직종"} el = call("groupRuleElement", "노임", "이름", port) assert ( el["mode"] == "rule" and el["ref"] == "" and el["name"] == "노임" and el["slot"] == "노임" ) assert el["rule"] == {"field": "이름", "op": "같음", "from": port} assert call("checkGroupRule", el, []) is None assert call("checkGroupRule", {**el, "name": ""}, []) == "자리 이름을 적어 주세요" assert call("checkGroupRule", el, [el]) == "같은 이름의 알약이 이미 있음" no_pill = call("groupRuleElement", "노임", "이름", {"box": "", "name": ""}) assert "변수 알약" in call("checkGroupRule", no_pill, []) def test_옛_줄_입력과_자동값으로_다른_로직_입력을_읽음(): inputs = [{"이름": "뒷길이", "단위": "㎝"}, {"이름": "돌"}, {"이름": "쌓기"}] auto = {"뒷길이": 25, "돌": "깬잡석", "쌓기": "", "남는": 1} ref = call("rowRef", "GF000219", "돌쌓기 메쌓기(인력)", "원/㎡", inputs, auto) assert ref["key"] == "GF000219" assert [o["name"] for o in ref["outputs"]] == ["노무비", "재료비", "경비", "계"] assert ref["designs"] == [ {"name": "뒷길이", "unit": "㎝"}, {"name": "돌", "unit": "-"}, {"name": "쌓기", "unit": "-"}, ] assert ref["examples"] == {"뒷길이": 25, "돌": "깬잡석"} # 빈 글 · 입력에 없는 이름은 뺌 def test_검색어_틀_자리와_채움(): tpl = "레미콘 {굵은골재최대치수}-{강도}-{슬럼프}" assert call("templateSlots", tpl) == ["굵은골재최대치수", "강도", "슬럼프"] assert call("templateSlots", "{a} {a} {b}") == ["a", "b"] # 겹친 자리는 한 번 slots = [ {"name": "굵은골재최대치수", "from": {"box": "b2", "name": "굵은"}}, {"name": "강도", "from": {"box": "b2", "name": "강도"}}, {"name": "슬럼프", "from": {"box": "b2", "name": "슬럼프"}}, ] values = {"b2.굵은": 25, "b2.강도": 18, "b2.슬럼프": 120} got = call("fillTemplate", tpl, slots, values) assert got == {"text": "레미콘 25-18-120", "missing": []} part = call("fillTemplate", tpl, slots, {"b2.굵은": 25, "b2.강도": ""}) assert part["missing"] == ["강도", "슬럼프"] assert part["text"] == "레미콘 25-{강도}-{슬럼프}" # 빈 자리는 그대로 둠 def test_검색어_틀_규칙과_검사(): slots = [{"name": "a", "from": {"box": "b2", "name": "굵은"}}] rule = call("templateRule", "레미콘 {a}", slots) assert ( rule["field"] == "검색어" and rule["template"] == "레미콘 {a}" and rule["from"] == slots[0]["from"] ) el = call("materialElement", "rule", "콘크리트", {"구분": "레미콘"}, {"rule": rule}) assert call("checkMaterial", el, []) is None no_slot = call("templateRule", "레미콘", []) bad = call("materialElement", "rule", "콘크리트", {"구분": "레미콘"}, {"rule": no_slot}) assert "자리" in call("checkMaterial", bad, []) short = call("templateRule", "레미콘 {a}-{b}", slots) # 자리 둘인데 알약은 하나 two = call("materialElement", "rule", "콘크리트", {}, {"rule": short}) assert "알약" in call("checkMaterial", two, [])