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

This commit is contained in:
2026-10-01 20:16:35 +09:00
2 changed files with 96 additions and 6 deletions
+7 -6
View File
@@ -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
+89
View File
@@ -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))