Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
186 lines
7.6 KiB
Python
186 lines
7.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""PLAN 208-1 — 단가산출 로직 「별칭」 칸 · 로직 찾기는 이름 + 별칭.
|
||
|
||
① 서버 찾기 맞춤(`M01_MasterData_Store_Find.hit`) ↔ 화면 공용 맞춤(`ui_template_logic_find.ts`) 거울
|
||
② 읽기(없으면 빈칸) · 저장 왕복 — 임시 사본 폴더만(정본 마스터 파일은 안 바뀜)
|
||
③ M01 목록 「별칭」 열(이름 옆) · 쓰는 곳마다 공용 맞춤을 부름
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
|
||
from M01_MasterData import M01_MasterData_Router as router_module
|
||
from M01_MasterData import M01_MasterData_Store as store
|
||
from M01_MasterData.M01_MasterData_Store_Find import hit
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
REAL = store.FOLDER
|
||
LOGIC_KEY = "GC000091" # 공통 3-3-1 암발파(미진동굴착 TYPE-Ⅰ)
|
||
LOGIC_FILE = "로직_건설품셈_03장_토공사.json"
|
||
|
||
ROW = {
|
||
"키": "GF000001",
|
||
"원문번호": "9-1-1",
|
||
"이름": "흙깎기(보통토사)",
|
||
"별칭": "절토, 터파기 기계",
|
||
}
|
||
# (찾는 글, 맞음, 별칭으로 맞은 것)
|
||
CASES = [
|
||
("흙깎기", True, ""),
|
||
("흙 깎기", True, ""), # 띄어쓰기 무시 · 낱말마다
|
||
("절토", True, "절토"),
|
||
("터파기", True, "터파기 기계"),
|
||
("터파기기계", True, "터파기 기계"), # 별칭 안 띄어쓰기 무시
|
||
("절토 기계", True, "절토, 터파기 기계"), # 낱말이 여러 별칭에 나뉘어 듦
|
||
("흙깎기 절토", True, "절토"), # 이름 + 별칭
|
||
("gf000001", True, ""),
|
||
("9-1", True, ""),
|
||
("성토", False, ""),
|
||
("", True, ""),
|
||
]
|
||
|
||
|
||
def _node(src: str) -> str:
|
||
if shutil.which("node") is None:
|
||
pytest.skip("node 가 없음")
|
||
done = subprocess.run(
|
||
["node", "--experimental-strip-types", "--input-type=module", "-e", src],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
timeout=60,
|
||
cwd=ROOT,
|
||
)
|
||
assert done.returncode == 0, done.stderr
|
||
return done.stdout
|
||
|
||
|
||
def test_서버_맞춤_이름_별칭_여러_별칭() -> None:
|
||
for q, ok, _ in CASES:
|
||
assert hit(ROW, q) is ok, q
|
||
assert hit({**ROW, "별칭": ""}, "절토") is False # 별칭 없음 = 빈칸
|
||
assert hit({k: v for k, v in ROW.items() if k != "별칭"}, "흙깎기") is True
|
||
|
||
|
||
def test_화면_맞춤_거울_서버와_같음() -> None:
|
||
url = (ROOT / "ui_template" / "ui_template_logic_find.ts").as_uri()
|
||
src = f"""
|
||
import {{ matchLogic, aliasText, aliasesOf, aliasNote }} from "{url}";
|
||
const row = {json.dumps(ROW, ensure_ascii=False)};
|
||
const cases = {json.dumps([c[0] for c in CASES], ensure_ascii=False)};
|
||
console.log(JSON.stringify({{
|
||
hits: cases.map((q) => matchLogic(q, row)),
|
||
text: aliasText(" 절토 ,, 터파기 기계,"),
|
||
none: aliasesOf(undefined),
|
||
note: [aliasNote("절토"), aliasNote("")],
|
||
chapter: matchLogic("토공", {{ ...row, 상세구분: "09장 토공" }}).ok,
|
||
}}));
|
||
"""
|
||
got = json.loads(_node(src))
|
||
for (q, ok, alias), one in zip(CASES, got["hits"]):
|
||
assert one == {"ok": ok, "alias": alias}, q
|
||
assert one["ok"] is hit(ROW, q), q # 서버와 같은 판정
|
||
assert got["text"] == "절토, 터파기 기계"
|
||
assert got["none"] == []
|
||
assert got["note"] == ["별칭 절토", ""]
|
||
assert got["chapter"] is True # 구분 · 상세구분은 화면만 더 봄
|
||
|
||
|
||
@pytest.fixture
|
||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||
for path in REAL.glob("*.json"):
|
||
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
|
||
shutil.copy(path, tmp_path / path.name)
|
||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||
app = FastAPI()
|
||
app.include_router(router_module.router)
|
||
return TestClient(app)
|
||
|
||
|
||
def _logics(client: TestClient, **params) -> list[dict]:
|
||
res = client.get("/api/m01/logics", params=params)
|
||
assert res.status_code == 200, res.text
|
||
return res.json()["logics"]
|
||
|
||
|
||
def _save(client: TestClient, row: dict) -> None:
|
||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||
part = {
|
||
"file": one["file"],
|
||
"version": one["version"],
|
||
"changes": [{"op": "edit", "key": LOGIC_KEY, "row": row}],
|
||
}
|
||
res = client.post("/api/m01/save", json={"files": [part]})
|
||
assert res.status_code == 200, res.text
|
||
|
||
|
||
def test_읽기_빈칸_저장_왕복_임시_사본만(client: TestClient) -> None:
|
||
real = (REAL / LOGIC_FILE).read_bytes()
|
||
listed = _logics(client)
|
||
assert listed and all(isinstance(x["별칭"], str) for x in listed) # 없으면 빈칸
|
||
one = client.get("/api/m01/logic", params={"key": LOGIC_KEY}).json()
|
||
base = one["logic"]
|
||
assert "별칭" not in base # 마스터 파일에 일괄로 쓰지 않음
|
||
|
||
_save(client, {**base, "별칭": "미진동 발파, 암깨기"})
|
||
got = [x for x in _logics(client, q="암깨기") if x["키"] == LOGIC_KEY]
|
||
assert got and got[0]["별칭"] == "미진동 발파, 암깨기"
|
||
assert LOGIC_KEY in [x["키"] for x in _logics(client, q="미진동 발파")]
|
||
found = client.get("/api/m01/elements", params={"group": "로직", "q": "암깨기"}).json()
|
||
assert [x["별칭"] for x in found["items"] if x["ref"] == LOGIC_KEY] == ["미진동 발파, 암깨기"]
|
||
saved = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||
assert next(r for r in saved["줄"] if r["키"] == LOGIC_KEY)["별칭"] == "미진동 발파, 암깨기"
|
||
|
||
_save(client, base) # 지우면 칸째 빠짐 · 찾기에도 안 걸림
|
||
assert LOGIC_KEY not in [x["키"] for x in _logics(client, q="암깨기")]
|
||
after = json.loads((store.FOLDER / LOGIC_FILE).read_text(encoding="utf-8"))
|
||
assert after == json.loads(real.decode("utf-8")) # 왕복 뒤 내용 그대로
|
||
assert (REAL / LOGIC_FILE).read_bytes() == real # 정본은 안 건드림
|
||
|
||
|
||
def _src(rel: str) -> str:
|
||
return (ROOT / rel).read_text(encoding="utf-8")
|
||
|
||
|
||
def test_M01_목록_별칭_열은_이름_옆_고치면_캐시() -> None:
|
||
ts = _src("M01_MasterData/M01_MasterData_UI_Logic_List.ts")
|
||
cols = ts[ts.index("const COLS") : ts.index("as const")]
|
||
names = [x.strip().strip('"') for x in cols[cols.index("[") + 1 :].split(",")]
|
||
assert names[names.index("이름") + 1] == "별칭" # 이름 바로 옆
|
||
assert "aliasCell(item" in ts and "onAlias(item, next)" in ts
|
||
assert "e.stopPropagation()" in ts # 별칭 칸을 눌러도 로직이 안 열림
|
||
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
|
||
assert "const onAlias = async" in lab and "list.setMarks(marks, extra, aliases)" in lab
|
||
# 자동저장 없음 — 캐시(drafts)에만 · [저장] 이 보냄
|
||
body = lab[lab.index("const onAlias = async") : lab.index("const onDelete = async")]
|
||
assert "saveFiles" not in body and "persist()" in body
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"rel",
|
||
[
|
||
"M01_MasterData/M01_MasterData_UI_Logic_List.ts", # M01 로직 목록
|
||
"M01_MasterData/M01_MasterData_UI_Logic_Pick.ts", # M01 요소 찾기 창 · 로직 그룹
|
||
"M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts", # M02 흐름도 다른 로직 고르기
|
||
"M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Logic.ts", # 〃 결과 별칭 표시
|
||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts", # M02 · B09 [로직 더하기]
|
||
],
|
||
)
|
||
def test_쓰는_곳마다_공용_맞춤을_부름(rel: str) -> None:
|
||
ts = _src(rel)
|
||
assert 'from "@ui/ui_template_logic_find"' in ts
|
||
assert "matchLogic(" in ts
|
||
|
||
|
||
def test_서버_찾기도_한_벌() -> None:
|
||
py = _src("M01_MasterData/M01_MasterData_Store.py")
|
||
assert "import hit as _hit" in py and "def _hit" not in py
|