Files
Aislo/resources/tester/test_m01_logic_flowview.py
T
eomsangdonandClaude Opus 5.5 85e11265b5 Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_7
충돌 넷 풂 — 합계 끝수 정본 = CfLogic.totalRound(totalRoundOf(lg, fallback) 하나 · 옮긴 로직 = 자신 끝수 · null = 없음 · 안 정함 = 기본 1원 버림) · sub5 줄 · 소계 꼴 살림 · sub1 fillTemplate(InputPanel_Build) 살리고 Entry_Detail 의 같은 함수 뺌 · flowview 시험을 정본 뜻으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
2026-10-02 01:02:21 +09:00

216 lines
7.5 KiB
Python

"""흐름 로직 왼쪽 「단가산출 상세」 줄 만들기(`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 r = load(path.join(root, "M02_MasterTemplete", "DataFlow", "M02_DF_Round.ts"));
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
const out = jobs.map((job) => job.op === "totalRound" ? r.totalRoundOf(job.lg, job.fallback) : 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"] == ["끊김"]
def test_total_round_follows_converted_logic_own_round():
"""합계 끝수 정본 = CfLogic.totalRound — 옮긴 로직 = 자신 끝수(null = 없음) · 안 정함 = 기본."""
default = {"places": 0, "method": "버림"}
own = {"places": 1, "method": "반올림"}
converted = {**logic([out_box("o1", "인건비", "노무비")]), "totalRound": own}
no_round = {**logic([out_box("o1", "인건비", "노무비")]), "totalRound": None}
fresh = logic(
[out_box("o1", "인건비", "노무비", round_=own)]
) # 출력 상자 끝수는 합계 끝수 아님
got = run(
[
{"op": "totalRound", "lg": converted, "fallback": default},
{"op": "totalRound", "lg": no_round, "fallback": default},
{"op": "totalRound", "lg": fresh, "fallback": default},
]
)
assert got == [own, None, default]
def test_subtotal_and_total_use_converted_round():
own = {"places": 1, "method": "반올림"}
lg = logic([out_box("o1", "인건비", "노무비", round_=own)])
ev = result(
{"o1": 3216.7}, {"노무비": 3216.7, "재료비": 0, "경비": 0, "값들": {}, "계": 3216.7}
)
rows = run([{"lg": lg, "ev": ev, "round": {"item": ROUND["item"], "total": own}}])[0]
assert group(rows, "labor")["subText"] == "3,216.7"
assert rows["totalText"] == "3,216.7"