"""로직 만들기 판 단계 안내 — 답 → 로직 줄 짜기(`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, "돌": "깬돌"}