Merge remote-tracking branch 'origin/sub_laptop_8' into sub_laptop_4

This commit is contained in:
2026-10-01 09:43:07 +09:00
3 changed files with 976 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
"""쉬운 단가산출 2단계 — 표 보고 고르기(`M01_EasyCalc_Table.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" / "EasyCalc" / "M01_EasyCalc_Table.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 stub = new Proxy({}, { get: () => () => undefined });
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, () => stub);
const m = box.exports;
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
const out = jobs.map((job) => {
if (job.op === "read") return m.readFinds(job.formula);
if (job.op === "swap") {
const calls = m.readFinds(job.formula);
const swaps = calls.map((c, i) => {
const s = job.swaps[i];
return s ? m.writeFind(c.table, s.args ?? c.args, s.col ?? c.col) : null;
});
return m.replaceFinds(job.formula, calls, swaps);
}
if (job.op === "literal") return m.literal(job.value, job.rule ?? "");
return null;
});
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)
MESSANGI = "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공 * (1 + 증가율 / 100)"
def test_식_읽기_흔한_꼴():
[calls] = run([{"op": "read", "formula": MESSANGI}])
assert len(calls) == 1
c = calls[0]
assert c["table"] == "QF000421" and c["col"] == "석공"
assert [(a["key"], a["rhs"]) for a in c["args"]] == [
("뒷길이", "뒷길이"),
("돌", "돌"),
("쌓기", "쌓기"),
]
assert MESSANGI[c["start"] : c["end"]] == "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공"
def test_식_읽기_따옴표_쉼표_괄호_겹침():
f = (
"(찾기(QC000524, 형식=형식).특별인부 - 만약(정밀절단 == '아님', "
"찾기(QC000583, 구분='가, 나 (다)').값, 0)) / 시공량"
)
[calls] = run([{"op": "read", "formula": f}])
assert [c["table"] for c in calls] == ["QC000524", "QC000583"]
assert calls[1]["args"] == [{"key": "구분", "rhs": "'가, 나 (다)'"}]
assert calls[1]["col"] == "값"
def test_식_읽기_못_읽는_꼴과_없는_꼴():
out = run(
[
{"op": "read", "formula": "찾기(QF:{지역}, 구분='a').값"}, # {자리} 낀 키
{"op": "read", "formula": "찾기(QF000001, 구분='a'"}, # 안 닫힘
{"op": "read", "formula": "품 * 1.2"},
{"op": "read", "formula": "'찾기(QF000001, 구분=1).값'"}, # 따옴표 글 안
]
)
assert out[0] is None and out[1] is None
assert out[2] == [] and out[3] == []
def test_새_식_짜기_줄_고정과_값칸_바꾸기():
[new] = run(
[
{
"op": "swap",
"formula": MESSANGI,
"swaps": [
{
"args": [
{"key": "뒷길이", "rhs": "35"},
{"key": "돌", "rhs": "'깬잡석'"},
{"key": "쌓기", "rhs": "쌓기"},
],
"col": "보통인부",
}
],
}
]
)
assert new == (
"찾기(QF000421, 뒷길이=35, 돌='깬잡석', 쌓기=쌓기).보통인부 * (1 + 증가율 / 100)"
)
def test_새_식_짜기_둘째_찾기만_바꿈_나머지_글_그대로():
f = "찾기(QC1, 구분='가새').수량 * 찾기(QC2, 공기=공기).손율 / 100"
[new] = run(
[
{
"op": "swap",
"formula": f,
"swaps": [None, {"args": [{"key": "공기", "rhs": "'1개월'"}]}],
}
]
)
assert new == "찾기(QC1, 구분='가새').수량 * 찾기(QC2, 공기='1개월').손율 / 100"
# 다시 읽어도 같은 모양(왕복)
[calls] = run([{"op": "read", "formula": new}])
assert calls[1]["args"] == [{"key": "공기", "rhs": "'1개월'"}]
def test_칸_값을_식_글로():
out = run(
[
{"op": "literal", "value": 25},
{"op": "literal", "value": "깬잡석"},
{"op": "literal", "value": "it's"}, # 따옴표 든 글은 못 씀
{"op": "literal", "value": [20, 40], "rule": "초과·이하"},
{"op": "literal", "value": [None, 60], "rule": "이하"},
{"op": "literal", "value": [None, 60], "rule": "이상·미만"},
{"op": "literal", "value": [80, None], "rule": "초과·이하"},
{"op": "literal", "value": [80, None], "rule": "이상·미만"},
]
)
assert out == ["25", "'깬잡석'", None, "30", "60", "59", "81", "80"]