Files
Aislo/resources/tester/test_815_2_alias_column.py
T
eomsangdonandClaude Opus 5.5 285332c44a feat(815-2): M01 별칭 모아 보기 · 제안 → 승인 길
- M01 왼쪽 「별칭 장부」 → 「별칭 모아 보기」 — 별칭이 적힌 줄 전부(인력 · 기계 · 재료 · 유가 · 단가산출 기초) · 수기/승인 표시 · 갈래 거름 · 찾기 · 줄 누르면 그 요소 화면 그 줄 · 별칭 칸 고침 → [저장](그 줄 별칭만)
- 위 「승인 대기 n」 — [승인] = 그 요소 별칭에 콤마로 더함(속 콤마 = 큰따옴표 조각) · [반려] = 지움
- 서버 M01_MasterData_Alias 새로 — GET /api/m01/alias · POST save · approve · reject(시스템 관리자) · POST suggest(로그인만 · storage/_shared/별칭제안.json) · 풀이 = master_alias
- 옛 /api/m01/alias-ledger(모인 후보 · 마스터 장부 · 반려 기억) 걷음
- M01 저장 — 값이 같은 칸은 파일 글 그대로(화면 왕복 9.0 → 9 막음 · 그 줄 별칭만 바뀜)
- 대시보드 카드 이름 「별칭 모아 보기」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:02:00 +09:00

236 lines
11 KiB
Python

"""PLAN 815-2 — M01 요소 「별칭」 한 열(요구 764 · 765).
① 별칭 칸 저장 = 그 줄만 바뀜 · 앞뒤 빈칸 뗌 · 빈 글이면 칸 없음(임시 사본 폴더만)
② M01 길은 시스템 관리자만(아니면 403)
③ 마스터 기계 별칭 다섯 → 기계 줄 `별칭` · 옛 장부 파일 걷음
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util import common_util_auth as auth
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as store
ROOT = Path(__file__).resolve().parents[2]
REAL = store.FOLDER
MACHINE = "기계.json"
KEY = "EQ000001"
FIVE = {
"EQ000192": "아스팔트페이버(피니셔)",
"EQ000193": "아스팔트페이버(피니셔)",
**{f"EQ000{n}": "디젤 엔진" for n in range(497, 508)},
"EQ000400": "워터제트",
"EQ000425": "버킷준설기",
"EQ000248": "바이브레이트 봉상후렉시블",
}
@pytest.fixture
def copy(tmp_path: Path, monkeypatch) -> Path:
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)
return tmp_path
def _app(role: str = "SYSTEM_ADMIN") -> TestClient:
app = FastAPI()
app.dependency_overrides[auth.verify_session] = lambda: {
"user_id": 1,
"name": "시험",
"role": role,
"company_id": 1,
}
deps = [Depends(auth.verify_session), Depends(auth.require_system_admin)]
app.include_router(router_module.router, dependencies=deps)
return TestClient(app)
def _save_alias(client: TestClient, text: str) -> None:
one = client.get("/api/m01/rows", params={"file": MACHINE, "q": KEY}).json()
row = next(r for r in one["rows"] if r["키"] == KEY)
part = {
"file": MACHINE,
"version": one["version"],
"changes": [{"op": "edit", "key": KEY, "row": {**row, "별칭": text}}],
}
res = client.post("/api/m01/save", json={"files": [part]})
assert res.status_code == 200, res.text
def _around(text: str) -> tuple[str, str, str]:
"""(그 줄 앞 글, 그 줄 글, 뒤 글)."""
start = text.rfind("{", 0, text.index(f'"키": "{KEY}"'))
_, end = json.JSONDecoder().raw_decode(text, start)
return text[:start], text[start:end], text[end:]
def test_별칭_저장_그_줄만_빈칸이면_칸_없음(copy: Path) -> None:
client = _app()
before = (copy / MACHINE).read_bytes()
_save_alias(client, " 시험별칭A, 시험별칭B ")
after = (copy / MACHINE).read_bytes().decode("utf-8")
row = next(r for r in json.loads(after)["줄"] if r["키"] == KEY)
assert row["별칭"] == "시험별칭A, 시험별칭B" # 글 그대로 · 앞뒤 빈칸만 뗌
old, new = _around(before.decode("utf-8")), _around(after)
assert old[0] == new[0] and old[2] == new[2] # 그 줄 밖 글 · 서식 그대로
assert json.loads(new[1]) == {**json.loads(old[1]), "별칭": "시험별칭A, 시험별칭B"}
flat = lambda s: re.sub(r"\s", "", s) # noqa: E731
assert flat(new[1]) == flat(old[1])[:-1] + ',"별칭":"시험별칭A,시험별칭B"}' # 9.0 그대로
_save_alias(client, " ")
last = _around((copy / MACHINE).read_bytes().decode("utf-8"))
assert last[0] == old[0] and last[2] == old[2]
assert last[1] == old[1] # 빈 글 = 칸 없음 = 처음 글 그대로
def test_M01_길은_시스템_관리자만() -> None:
for role in ("USER", "COMPANY_ADMIN"):
res = _app(role).post("/api/m01/save", json={"files": []})
assert res.status_code == 403
main = (ROOT / "main.py").read_text(encoding="utf-8")
assert "include_router(m01_master_data_router, dependencies=system_admin_only)" in main
def test_마스터_기계_별칭_다섯_옮김_장부_걷음() -> None:
rows = {r["키"]: r for r in json.loads((REAL / MACHINE).read_text(encoding="utf-8"))["줄"]}
assert {k: r.get("별칭") for k, r in rows.items() if r.get("별칭")} == FIVE
assert not (ROOT / "resources" / "master_template" / "실무자료" / "맞춤.json").exists()
# ── 모아 보기 · 제안 → 승인 ───────────────────────────────────────────
@pytest.fixture
def suggest_file(tmp_path: Path, monkeypatch) -> Path:
from M01_MasterData import M01_MasterData_Alias as alias
path = tmp_path / "_shared" / "별칭제안.json"
monkeypatch.setattr(alias, "suggest_path", lambda: path)
return path
def _alias_app(role: str = "SYSTEM_ADMIN") -> TestClient:
from M01_MasterData import M01_MasterData_Alias as alias
app = FastAPI()
app.dependency_overrides[auth.verify_session] = lambda: {
"user_id": 7,
"name": "시험",
"role": role,
"company_id": 1,
}
admin = [Depends(auth.verify_session), Depends(auth.require_system_admin)]
app.include_router(alias.router, dependencies=admin)
app.include_router(alias.suggest_router, dependencies=[Depends(auth.verify_session)])
return TestClient(app)
def test_모아_보기_다섯_수기_넣은_것_바로(copy: Path, suggest_file: Path) -> None:
client = _alias_app()
rows = client.get("/api/m01/alias").json()["줄"]
assert {r["키"]: r["별칭"] for r in rows if r["갈래"] == "기계"} == FIVE
assert all(r["들어옴"] == "수기" for r in rows)
one = next(r for r in rows if r["키"] == "EQ000248")
assert [p["글"] for p in one["조각"]] == ["바이브레이트 봉상후렉시블"]
res = client.post("/api/m01/alias/save", json={"키": "LB000001", "별칭": "시험A,시험B"})
assert res.status_code == 200, res.text
got = next(r for r in res.json()["줄"] if r["키"] == "LB000001")
assert got["갈래"] == "인력" and [p["글"] for p in got["조각"]] == ["시험A", "시험B"]
res = client.post("/api/m01/alias/save", json={"키": "LB000001", "별칭": " "})
assert "LB000001" not in [r["키"] for r in res.json()["줄"]]
assert (
client.post("/api/m01/alias/save", json={"키": "ZZ999999", "별칭": "x"}).status_code == 404
)
def test_제안_승인_별칭에_더함_반려_지움(copy: Path, suggest_file: Path) -> None:
user, admin = _alias_app("USER"), _alias_app()
assert user.get("/api/m01/alias").status_code == 403 # 모아 보기 · 승인 = 시스템 관리자만
body = {"키": "EQ000400", "이름": "워터 젯", "규격": "96kW, 고압", "단위": "hr"}
sid = user.post("/api/m01/alias/suggest", json=body).json()["id"]
again = user.post("/api/m01/alias/suggest", json={**body, "이름": "워터제트", "규격": ""})
assert again.json() == {"이미": True} # 이미 있는 별칭은 제안 안 남음
assert user.post("/api/m01/alias/approve", json={"id": sid}).status_code == 403
wait = admin.get("/api/m01/alias").json()["대기"]
assert [(w["id"], w["갈래"], w["별칭"], w["누가"]["id"]) for w in wait] == [
(sid, "기계", "워터 젯 96kW, 고압", 7)
]
got = admin.post("/api/m01/alias/approve", json={"id": sid}).json()
row = next(r for r in got["줄"] if r["키"] == "EQ000400")
assert row["별칭"] == '워터제트, "워터 젯 96kW, 고압"' # 속 콤마 = 큰따옴표 조각
assert [(p["글"], p["승인"]) for p in row["조각"]] == [
("워터제트", False),
("워터 젯 96kW, 고압", True),
]
assert row["들어옴"] == "승인" and got["대기"] == []
data = json.loads((copy / MACHINE).read_text(encoding="utf-8"))
assert next(r for r in data["줄"] if r["키"] == "EQ000400")["별칭"] == row["별칭"]
assert admin.post("/api/m01/alias/approve", json={"id": sid}).status_code == 404
sid2 = user.post("/api/m01/alias/suggest", json={**body, "키": "EQ000425"}).json()["id"]
got = admin.post("/api/m01/alias/reject", json={"id": sid2}).json()
assert got["대기"] == [] and sid2 not in json.loads(suggest_file.read_text("utf-8"))["제안"]
assert user.post("/api/m01/alias/suggest", json={**body, "키": "LT000001"}).status_code == 404
# ── 옛 별칭 장부 · 연결 찾기 걷음 ──────────────────────────────────────
def test_옛_장부_길_연결_찾기_길_404() -> None:
from M02_MasterTemplete import M02_Doc_Router as doc_router
from M02_MasterTemplete import M02_Hopyo_Router as hopyo_router
app = FastAPI()
app.dependency_overrides[auth.verify_session] = lambda: {"user_id": 1, "role": "SYSTEM_ADMIN"}
for r in (hopyo_router.router, doc_router.router, _alias_router()):
app.include_router(r)
client = TestClient(app)
for method, url in (
("get", "/api/m02/layers/personal/hopyo-ledger"),
("put", "/api/m02/layers/personal/hopyo-ledger"),
("post", "/api/m02/layers/personal/sangeun/SK-000001/match"),
("get", "/api/m01/alias-ledger"),
("post", "/api/m01/alias-ledger/approve"),
):
assert getattr(client, method)(url).status_code == 404, url
def _alias_router():
from M01_MasterData import M01_MasterData_Alias as alias
return alias.router
def test_걷은_파일_쓰는_곳_0() -> None:
gone = (
"M02_MasterTemplete/M02_Doc_Match.py",
"M02_MasterTemplete/M02_Doc_Match_Note.py",
"M02_MasterTemplete/M02_Doc_Match_Value.py",
"M02_MasterTemplete/M02_Doc_MatchSk.py",
"M02_MasterTemplete/M02_MasterTemplete_Sangeun_MatchBand.ts",
"M02_MasterTemplete/logic/M02_Sangeun_Match.ts",
"M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts",
)
assert not [g for g in gone if (ROOT / g).exists()]
names = [Path(g).stem for g in gone]
listed = subprocess.run(
["git", "ls-files", "*.py", "*.ts"], cwd=ROOT, capture_output=True, text=True
).stdout.split()
for one in listed:
path = ROOT / one
if "old_code" in path.parts or "tester" in path.parts or not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="ignore")
assert not [n for n in names if f"import {n}" in text or f'/{n}"' in text], path
build = (ROOT / "M02_MasterTemplete" / "M02_Hopyo_Build.py").read_text(encoding="utf-8")
assert "ledger.write(" not in build # 일위대가 불러오기 확인 = 장부에 안 적음
doc = (ROOT / "M02_MasterTemplete" / "M02_Doc_Router.py").read_text(encoding="utf-8")
assert "mfind.learn" not in doc # 가져온 식 저장 = 장부에 안 적음