# -*- 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