Files
Aislo/M01_MasterData/M01_MasterData_Alias.py
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

235 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M01 별칭 모아 보기 · 제안 → 승인(PLAN 815-2 · 요구 764 · 765) — 서버 단독.
별칭 = 요소 · 로직 줄 칸 `별칭` 하나(콤마 여러 개 · 풀이 정본 `master_alias`).
GET /api/m01/alias → {줄: 별칭이 적힌 줄 전부(갈래 · 키 · 이름 · 규격 · 별칭 · 조각 ·
들어옴 수기/승인), 대기: 승인 대기 제안}
POST /api/m01/alias/save {키, 별칭} → 그 줄 `별칭` 만 고침(M01 저장 길 `make.save`)
POST /api/m01/alias/approve {id} → 그 요소 `별칭` 에 콤마로 더함 · 제안 = 「승인」 기록
POST /api/m01/alias/reject {id} → 제안 지움
POST /api/m01/alias/suggest {키, 이름, 규격?, 단위?(갈래는 서버가 키로 정함)} → 제안 하나(로그인만 · 일반 계정도)
제안 파일 = `storage/_shared/별칭제안.json` `{제안: {id: {…, 상태 대기|승인}}}`.
권한 = `router` 시스템 관리자 · `suggest_router` 로그인만(`main.py`).
"""
from __future__ import annotations
import hashlib
import json
import os
import threading
from pathlib import Path
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from common_util.common_util_auth import verify_session
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Make as make
from M02_MasterTemplete import M02_Template_Layers as layers
import master_alias as ma # noqa: E402 (`store` 가 scripts 를 길에 둠)
ALIAS = "별칭"
FUEL = "재료_유가전력.json"
ELEMENTS = ("인력.json", "기계.json", "재료_자재품목.json", FUEL) # 제안을 받는 요소 파일
_LOCK = threading.Lock()
router = APIRouter(prefix="/api/m01/alias", tags=["M01 별칭"])
suggest_router = APIRouter(prefix="/api/m01/alias", tags=["M01 별칭"])
def kind_of(file: str, group: str) -> str:
if file == FUEL:
return "유가"
return "단가산출 기초" if group == "로직" else group
# ── 제안 파일 ─────────────────────────────────────────────────────────
def suggest_path() -> Path:
return layers.storage_root() / "_shared" / "별칭제안.json"
def _book() -> dict[str, dict[str, Any]]:
try:
got = json.loads(suggest_path().read_text(encoding="utf-8")).get("제안")
except (OSError, ValueError, AttributeError):
return {}
return got if isinstance(got, dict) else {}
def _keep(book: dict[str, dict[str, Any]]) -> None:
path = suggest_path()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps({"제안": book}, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(tmp, path)
# ── 줄 찾기 · 고치기 ───────────────────────────────────────────────────
def _find(key: str, files: tuple[str, ...] | None = None) -> tuple[str, dict[str, Any]]:
loaded, _ = store.loaded()
for name, data in loaded.items():
if files and name not in files:
continue
for row in data.get("줄") or []:
if str(row.get("키")) == key:
return name, row
raise store.StoreError(404, f"없는 키 「{key}」")
def set_alias(key: str, text: str) -> dict[str, Any]:
"""그 줄 `별칭` 만 바꿔 M01 저장 길로(빈 글 = 칸 없음 · 별칭만이라 정본 개정 확인 안 물음)."""
file, _ = _find(key)
data, version = store.read(file)
row = dict(next(r for r in data["줄"] if str(r.get("키")) == key))
row[ALIAS] = text
change = {"op": "edit", "key": key, "row": row}
make.save([{"file": file, "version": version, "changes": [change]}], True)
return board()
def _quoted(piece: str) -> str:
return f'"{piece}"' if any(s in piece for s in (",", ",")) else piece
def _joined(text: Any, piece: str) -> str:
"""별칭 글 끝에 한 조각 — 이미 있으면(정규화 같음) 그대로."""
raw = str(text or "").strip()
if ma.norm(piece) in {ma.norm(x) for x in ma.names(raw)}:
return raw
return f"{raw}, {_quoted(piece)}" if raw else _quoted(piece)
# ── 모아 보기 ─────────────────────────────────────────────────────────
def board() -> dict[str, Any]:
book = _book()
approved = {
(str(v.get("키")), ma.norm(v.get(ALIAS))) for v in book.values() if v.get("상태") == "승인"
}
rows = []
loaded, _ = store.loaded()
for file, data in loaded.items():
kind = kind_of(file, str(data.get("그룹") or ""))
for row in data.get("줄") or []:
if not str(row.get(ALIAS) or "").strip():
continue
key = str(row.get("키"))
pieces = [
{"글": p, "승인": (key, ma.norm(p)) in approved} for p in ma.names(row[ALIAS])
]
rows.append(
{
"갈래": kind,
"파일": file,
"키": key,
"이름": str(row.get("이름") or ""),
"규격": str(row.get("규격") or ""),
ALIAS: str(row[ALIAS]),
"조각": pieces,
"들어옴": "승인" if any(p["승인"] for p in pieces) else "수기",
}
)
waiting = [{"id": k, **v} for k, v in book.items() if v.get("상태") == "대기"]
waiting.sort(key=lambda x: str(x.get("때") or ""))
return {"줄": rows, "대기": waiting}
# ── 제안 · 승인 · 반려 ────────────────────────────────────────────────
def suggest(key: str, name: str, spec: str, unit: str, who: Any) -> dict[str, Any]:
file, row = _find(key, ELEMENTS)
piece = " ".join(x for x in (name.strip(), spec.strip()) if x)
if not piece:
raise store.StoreError(400, "가져온 이름이 비었음")
if _joined(row.get(ALIAS), piece) == str(row.get(ALIAS) or "").strip():
return {"이미": True} # 이미 그 별칭이 있음
sid = hashlib.sha256(f"{key}|{ma.norm(piece)}".encode()).hexdigest()[:16]
with _LOCK:
book = _book()
if book.get(sid, {}).get("상태") != "승인":
book[sid] = {
"갈래": kind_of(file, str(store.loaded()[0][file].get("그룹") or "")),
"키": key,
"이름": name.strip(),
"규격": spec.strip(),
"단위": unit.strip(),
ALIAS: piece,
"상태": "대기",
"누가": who,
"때": utc_now_text(),
}
_keep(book)
return {"id": sid}
def decide(sid: str, ok: bool, who: Any) -> dict[str, Any]:
with _LOCK:
book = _book()
one = book.get(sid)
if not one or one.get("상태") != "대기":
raise store.StoreError(404, "대기 중인 제안이 없음(이미 승인 · 반려)")
if ok:
_, row = _find(str(one["키"]), ELEMENTS)
set_alias(str(one["키"]), _joined(row.get(ALIAS), str(one[ALIAS])))
one.update(상태="승인", 승인={"누가": who, "때": utc_now_text()})
else:
del book[sid]
_keep(book)
return board()
# ── 길 ───────────────────────────────────────────────────────────────
def _who(session: dict[str, Any]) -> dict[str, Any]:
return {"id": session.get("user_id"), "이름": session.get("name")}
def _call(fn, *args):
try:
return fn(*args)
except store.StoreError as error:
raise HTTPException(error.status, error.detail) from error
class AliasBody(BaseModel):
키: str = Field(min_length=1, max_length=20)
별칭: str = Field(max_length=2000)
class Decide(BaseModel):
id: str = Field(min_length=16, max_length=16)
class Suggest(BaseModel):
갈래: str = Field(default="", max_length=20)
키: str = Field(min_length=1, max_length=20)
이름: str = Field(min_length=1, max_length=200)
규격: str = Field(default="", max_length=200)
단위: str = Field(default="", max_length=40)
@router.get("")
def get_board() -> dict[str, Any]:
return board()
@router.post("/save")
def post_save(body: AliasBody) -> dict[str, Any]:
return _call(set_alias, body.키, body.별칭)
@router.post("/approve")
def post_approve(body: Decide, session: dict = Depends(verify_session)) -> dict[str, Any]:
return _call(decide, body.id, True, _who(session))
@router.post("/reject")
def post_reject(body: Decide, session: dict = Depends(verify_session)) -> dict[str, Any]:
return _call(decide, body.id, False, _who(session))
@suggest_router.post("/suggest")
def post_suggest(body: Suggest, session: dict = Depends(verify_session)) -> dict[str, Any]:
args = (body.키, body.이름, body.규격, body.단위, _who(session))
return _call(suggest, *args)