fix(m02-df): 자재 찾기 띄어쓰기 지우고 비교 · 선 규칙 화면(Rule.ts) = 서버 시험
- /materials — 품목 글(이름 · 규격 · 키 · 원문번호) · 찾는 낱말 모두 띄어쓰기를 지우고 포함(AND) — 「흄관 300」 3 → 8 · 「흄관」 45 → 117 - 시험 — 이름 속 띄어쓰기 · Rule.ts 를 Node 로 그대로 돌려 서버 apply_rule 과 25가지 맞댐(표 찾기 범위 · 숫자 하나 = 같음 · 여럿 · 없음 · 조건 · 바꾸기 · 끝수) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
@@ -298,18 +298,19 @@ def _squash(text: Any) -> str:
|
||||
|
||||
|
||||
def _found(sub: str, detail: str, q: str) -> list[dict]:
|
||||
"""범위(구분 · 상세구분) 안 · 띄어쓰기로 나눈 말을 모두(AND) 이름 · 규격 · 키 · 원문번호에서."""
|
||||
words = [w.lower() for w in str(q or "").split()]
|
||||
"""범위(구분 · 상세구분) 안 · 띄어쓰기로 나눈 말을 모두(AND) 이름 · 규격 · 키 · 원문번호에서.
|
||||
|
||||
품목 글 · 찾는 낱말 모두 띄어쓰기를 지우고 비교 — 「흄 관(B형)」 도 「흄관」 으로 걸림.
|
||||
"""
|
||||
words = [_squash(w) for w in str(q or "").split()]
|
||||
numbers = _numbers()
|
||||
rows = (store.loaded()[0].get(MATERIALS) or {}).get("줄") or []
|
||||
out = []
|
||||
for r in rows:
|
||||
if (sub and r.get("구분") != sub) or (detail and r.get("상세구분") != detail):
|
||||
continue
|
||||
hay = " ".join(
|
||||
str(x or "")
|
||||
for x in (r.get("이름"), r.get("규격"), r.get("키"), numbers.get(r.get("키")))
|
||||
).lower()
|
||||
fields = (r.get("이름"), r.get("규격"), r.get("키"), numbers.get(r.get("키")))
|
||||
hay = "|".join(_squash(x) for x in fields)
|
||||
if all(w in hay for w in words):
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
@@ -15,6 +15,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
@@ -28,6 +29,7 @@ from M01_MasterData import M01_MasterData_Store as store
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Router as cf
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Values as dv
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REAL = store.FOLDER
|
||||
OWN = "로직_자체.json"
|
||||
SAMPLE = "QF999995"
|
||||
@@ -607,3 +609,90 @@ def test_새_길도_시스템_관리자만(folder: Path) -> None:
|
||||
):
|
||||
assert getattr(client, method)(url).status_code == 403 # fmt: skip
|
||||
assert client.post("/api/m02/df/logic-value", json={"key": "x"}).status_code == 403
|
||||
|
||||
|
||||
def test_자재_찾기_이름_속_띄어쓰기(client: TestClient, folder: Path) -> None:
|
||||
"""품목 글 · 찾는 낱말 모두 띄어쓰기를 지우고 비교 — 「흄 관(B형)」 도 「흄관」 으로 걸림."""
|
||||
path = folder / "재료_자재품목.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
row = dict.fromkeys(data["줄"][0])
|
||||
row.update(
|
||||
{
|
||||
"키": "MT999990",
|
||||
"구분": "시험구분",
|
||||
"이름": "견 본 관(B형)",
|
||||
"규격": "Φ 300mm",
|
||||
"물가자료": 1,
|
||||
}
|
||||
)
|
||||
data["줄"].append(row)
|
||||
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
for q in ("견본관 300", "견 본관 Φ300", "본관(b형)"):
|
||||
got = client.get("/api/m02/df/materials", params={"q": q, "sub": "시험구분"}).json()
|
||||
assert [x["ref"] for x in got["items"]] == ["MT999990"], q
|
||||
|
||||
|
||||
RULE_RUNNER = r"""
|
||||
const fs = require("fs"), path = require("path");
|
||||
const [root, jobs] = [process.argv[1], JSON.parse(process.argv[2])];
|
||||
const ts = require(path.join(root, "config", "node_modules", "typescript"));
|
||||
const cache = {};
|
||||
function load(file) {
|
||||
if (cache[file]) return cache[file].exports;
|
||||
const m = { exports: {} };
|
||||
cache[file] = m;
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const req = (spec) => {
|
||||
if (spec === "@ui/ui_template_locale") return { currentLanguageIndex: 0 };
|
||||
if (spec.endsWith(".css")) return {};
|
||||
const base = spec.startsWith("@ui/") ? path.join(root, "ui_template", spec.slice(4))
|
||||
: spec.startsWith("@config/") ? path.join(root, "config", spec.slice(8))
|
||||
: spec.startsWith(".") ? path.resolve(path.dirname(file), spec) : null;
|
||||
if (!base) return require(spec);
|
||||
const hit = [base + ".ts", base].find((c) => fs.existsSync(c) && fs.statSync(c).isFile());
|
||||
if (!hit) throw new Error("못 찾음 " + spec);
|
||||
return hit.endsWith(".ts") ? load(hit) : require(hit);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(m.exports, m, req);
|
||||
return m.exports;
|
||||
}
|
||||
const rule = load(path.join(root, "M02_MasterTemplete", "DataFlow", "M02_DF_Rule.ts"));
|
||||
console.log(JSON.stringify(jobs.map((j) => { const r = rule.applyRule(j.rule, j.value, j.conds); return [r.value, r.why]; })));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(NO_NODE, reason="node 가 없음")
|
||||
def test_선_규칙_화면_Rule_ts_와_서버가_같은_값() -> None:
|
||||
"""화면 `M02_DF_Rule.applyRule`(Node 로 그대로) = 서버 `apply_rule` — 값 · 까닭."""
|
||||
block = [["돌", "높이·아래", "높이·위", "석공"], ["깬돌", None, 3, 0.5], ["깬돌", 3, 7.5, 0.6],
|
||||
["깬돌", 7.5, None, 0.8], ["견치돌", 4, 4, 0.7], ["견치돌", 4, 4, 0.9]] # fmt: skip
|
||||
look = {"kind": "lookup", "col": "석공", "conds": [{"col": "돌", "from": {"box": "v", "name": "돌"}},
|
||||
{"col": "높이", "from": {"box": "v", "name": "높이"}}]} # fmt: skip
|
||||
gate = {"kind": "gate", "from": {"box": "v", "name": "높이"}, "op": "≥", "value": 3}
|
||||
jobs = [{"rule": look, "value": block, "conds": {"돌": d, "높이": h}}
|
||||
for d, h in (("깬돌", 2), ("깬돌", 3), ("깬돌", 3.1), ("깬돌", 7.5), ("깬돌", 100), ("깬돌", -5),
|
||||
("견치돌", 4), ("견치돌", 5), ("없는돌", 1))] # fmt: skip
|
||||
jobs += [{"rule": gate, "value": 10, "conds": {"높이": h}} for h in (2, 3, 4)]
|
||||
jobs += [{"rule": {"kind": "gate", "from": {"box": "v", "name": "s"}, "op": op, "value": "가"},
|
||||
"value": 1, "conds": {"s": s}} for op, s in (("=", "가"), ("≠", "가"), ("=", "나"))] # fmt: skip
|
||||
jobs += [{"rule": {"kind": "scale", "op": op, "value": 7}, "value": 123.45, "conds": {}}
|
||||
for op in ("×", "÷", "+%", "-%")] # fmt: skip
|
||||
jobs += [{"rule": {"kind": "round", "places": p, "method": m}, "value": v, "conds": {}}
|
||||
for p, m, v in ((0, "반올림", 2.5), (1, "올림", 1.21), (-1, "버림", 1239), (-3, "반올림", 45500),
|
||||
(2, "버림", -1.239), (0, "반올림", -2.5))] # fmt: skip
|
||||
result = subprocess.run(
|
||||
["node", "-e", RULE_RUNNER, str(ROOT), json.dumps(jobs, ensure_ascii=False)],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=120,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
screen = json.loads(result.stdout)
|
||||
server = [cf.dc.apply_rule(j["rule"], j["value"], j["conds"]) for j in jobs]
|
||||
for job, (sv, sw), (cv, cw) in zip(jobs, screen, server):
|
||||
same = (sv is None and cv is None) or (
|
||||
sv is not None
|
||||
and cv is not None
|
||||
and abs(Decimal(str(sv)) - Decimal(str(cv))) < Decimal("1e-9")
|
||||
)
|
||||
assert same and sw == cw, (job, (sv, sw), (cv, cw))
|
||||
|
||||
Reference in New Issue
Block a user