Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
255 lines
10 KiB
Python
255 lines
10 KiB
Python
"""단가산출 다이어그램 입력 상자 판 — 고르는 규칙(`M02_CF_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" / "CostFlow" / "M02_CF_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"]
|
|
== "㎡"
|
|
)
|