Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
"""로직 만들기 판 단계 안내 — 답 → 로직 줄 짜기(`M01_LB_Wizard_Build.ts`).
|
|
|
|
화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 순수 함수만 부름.
|
|
견본 표 · 답은 이 시험 안에 둠(진짜 마스터 내용에 기대지 않음).
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SOURCE = ROOT / "M01_MasterData" / "LogicBuilder" / "M01_LB_Wizard_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((job) => {
|
|
if (job.op === "unit") return m.unitOfBase(job.base);
|
|
if (job.op === "row") return m.buildRow(job.a);
|
|
if (job.op === "trial") return m.trialOf(job.a, m.buildRow(job.a));
|
|
return null;
|
|
});
|
|
process.stdout.write(JSON.stringify(out));
|
|
"""
|
|
|
|
TABLE = {
|
|
"key": "QF900001",
|
|
"이름": "견본 쌓기",
|
|
"기준": "1㎡",
|
|
"출처": "산림품셈 9-9-9",
|
|
"조건": {"뒷길이": "수", "돌": "고르기"},
|
|
"값칸": {"석공": "인", "보통인부": "인"},
|
|
"줄": [
|
|
{"뒷길이": 25, "돌": "깬돌", "석공": 0.15, "보통인부": 0.12},
|
|
{"뒷길이": 30, "돌": "깬돌", "석공": 0.26, "보통인부": 0.21},
|
|
],
|
|
"주": ["③ 높이 3m 초과 시 할증"],
|
|
}
|
|
RATE_TABLE = {
|
|
"key": "QF900002",
|
|
"이름": "견본 증가율표",
|
|
"기준": "",
|
|
"출처": "산림품셈 9-9-9 주③",
|
|
"조건": {"높이": "범위"},
|
|
"값칸": {"증가율": "%"},
|
|
"줄": [{"높이": [3, 4], "증가율": 30}],
|
|
"주": [],
|
|
}
|
|
|
|
|
|
def answers(**more) -> dict:
|
|
base = {
|
|
"book": "산림품셈",
|
|
"section": "9-9-9",
|
|
"table": TABLE,
|
|
"roles": {"뒷길이": "조건", "돌": "조건", "석공": "결과", "보통인부": "결과"},
|
|
"fixed": {},
|
|
"options": {"뒷길이": [25, 30], "돌": ["깬돌"]},
|
|
"resultUnit": "원/㎡",
|
|
"mods": [],
|
|
"links": [
|
|
{
|
|
"col": "석공",
|
|
"kind": "노무",
|
|
"name": "석공",
|
|
"spec": "",
|
|
"unit": "인",
|
|
"element": "LB000033",
|
|
},
|
|
{
|
|
"col": "보통인부",
|
|
"kind": "노무",
|
|
"name": "보통인부",
|
|
"spec": "",
|
|
"unit": "인",
|
|
"element": "LB000002",
|
|
},
|
|
],
|
|
"extras": [],
|
|
"name": "견본",
|
|
"kind": "금액",
|
|
"valueCol": "",
|
|
}
|
|
base.update(more)
|
|
return base
|
|
|
|
|
|
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 test_결과단위는_표_기준에서():
|
|
out = run([{"op": "unit", "base": b} for b in ("1㎡", "100㎡ 당", "㎡당", "m당", "")])
|
|
assert out == ["원/㎡", "원/100㎡", "원/㎡", "원/m", "원/㎡"]
|
|
|
|
|
|
def test_기본_두_줄():
|
|
row = run([{"op": "row", "a": answers()}])[0]
|
|
assert [i["이름"] for i in row["입력"]] == ["뒷길이", "돌"]
|
|
assert row["입력"][0]["고르기"] == [25, 30]
|
|
assert row["중간"] == []
|
|
assert [h["수량"] for h in row["호표"]] == [
|
|
"찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공",
|
|
"찾기(QF900001, 뒷길이=뒷길이, 돌=돌).보통인부",
|
|
]
|
|
assert row["호표"][0]["종류"] == "인력" and row["호표"][0]["비목"] == "노무비"
|
|
assert row["결과단위"] == "원/㎡" and row["출처"] == "산림품셈 9-9-9"
|
|
|
|
|
|
def test_고정칸은_값으로():
|
|
a = answers(
|
|
roles={"뒷길이": "고정", "돌": "조건", "석공": "결과", "보통인부": "쓰지 않음"},
|
|
fixed={"뒷길이": 25},
|
|
)
|
|
row = run([{"op": "row", "a": a}])[0]
|
|
assert [i["이름"] for i in row["입력"]] == ["돌"]
|
|
assert [h["수량"] for h in row["호표"]] == ["찾기(QF900001, 돌=돌, 뒷길이=25).석공"]
|
|
|
|
|
|
def test_문장틀_차례대로_씌움():
|
|
cond = {
|
|
"t": "cond",
|
|
"input": "높이",
|
|
"op": "<=",
|
|
"limit": 3,
|
|
"table": RATE_TABLE,
|
|
"col": "증가율",
|
|
}
|
|
a = answers(
|
|
mods=[
|
|
{"k": "할증", "rate": cond, "src": "산림품셈 9-9-9 주③"},
|
|
{"k": "나누기", "by": {"t": "input", "name": "시공량"}},
|
|
{"k": "올림", "fn": "반올림", "places": 3},
|
|
]
|
|
)
|
|
row = run([{"op": "row", "a": a}])[0]
|
|
assert row["중간"] == [
|
|
{
|
|
"이름": "증가율",
|
|
"식": "만약(높이 <= 3, 0, 찾기(QF900002, 높이=높이).증가율)",
|
|
"출처": "산림품셈 9-9-9 주③",
|
|
}
|
|
]
|
|
assert row["호표"][0]["수량"] == (
|
|
"반올림((찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공 * (1 + 증가율 / 100)) / 시공량, 3)"
|
|
)
|
|
assert [i["이름"] for i in row["입력"]] == ["뒷길이", "돌", "높이", "시공량"]
|
|
|
|
|
|
def test_비율_덧줄은_사람줄합():
|
|
extras = [{"k": "비율", "name": "공구손료", "base": "사람줄합", "rate": 3, "cost": "재료비"}]
|
|
row = run([{"op": "row", "a": answers(extras=extras)}])[0]
|
|
assert row["덧줄"] == [
|
|
{
|
|
"이름": "공구손료",
|
|
"식": "((찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공) * LB000033 + (찾기(QF900001, 뒷길이=뒷길이, 돌=돌).보통인부) * LB000002) * 3 / 100",
|
|
"비목": "재료비",
|
|
}
|
|
]
|
|
|
|
|
|
def test_값_종류는_결과_식_하나():
|
|
a = answers(kind="값", valueCol="석공", resultUnit="인/㎡")
|
|
row = run([{"op": "row", "a": a}])[0]
|
|
assert row["결과"] == {"식": "찾기(QF900001, 뒷길이=뒷길이, 돌=돌).석공"}
|
|
assert "호표" not in row and row["결과단위"] == "인/㎡"
|
|
|
|
|
|
def test_예시값은_첫_줄에_걸림():
|
|
out = run([{"op": "trial", "a": answers()}])[0]
|
|
assert out == {"뒷길이": 25, "돌": "깬돌"}
|