충돌 넷 풂 — 합계 끝수 정본 = 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
216 lines
7.5 KiB
Python
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"
|