"""Z01 마스터 데이터 라우터 — 읽기 + 기초단가 고치기(2026-09-15 브레인). GET /api/master-data/tree 갈래 → 파일 → 표 GET /api/master-data/rows?file=&table=&page=&size=&q= 표 줄(쪽 나누기 · 검색) GET /api/master-data/base-prices/{kind}?page=&size=&q=&sort=&desc= 기초단가 한 표(labor|machine|material|oil|rate) PUT /api/master-data/base-prices/{kind}/{row_id} {values:{열:값}} → 덮개에만 씀 · null = 되돌리기 GET /api/master-data/overrides 고친 것·원본 바뀐 것·주인 없는 것(서버 정렬) ⚠ 권한은 등록하는 쪽(`main.py` · 랩탑 서브)이 `dependencies=[verify_session, require_system_admin]` 로 붙임. """ from __future__ import annotations from typing import Any, Literal from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel from common_util.common_util_auth import verify_session from Z01_MasterData import Z01_MasterData_BasePrices as base_prices from Z01_MasterData import Z01_MasterData_Tables as tables router = APIRouter(prefix="/api/master-data", tags=["Z01 MasterData"]) class BasePriceEdit(BaseModel): values: dict[str, Any] @router.get("/tree") def get_tree() -> dict: return tables.tree() @router.get("/rows") def get_rows( file: str, table: str, page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE, q: str = "" ) -> dict: result = tables.rows(file, table, page=page, size=size, q=q) if result is None: raise HTTPException(status_code=404, detail="없는 파일이나 표입니다.") return result def _kind(kind: str) -> str: if kind not in base_prices.KINDS: raise HTTPException(status_code=404, detail=f"없는 기초단가: {kind}") return kind @router.get("/base-prices/{kind}") def get_base_prices( kind: str, page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE, q: str = "", sort: str = "", desc: int = 0, ) -> dict: try: return base_prices.table(_kind(kind), page=page, size=size, q=q, sort=sort, desc=bool(desc)) except base_prices.SortError as error: raise HTTPException(status_code=400, detail=str(error)) from error @router.put("/base-prices/{kind}/{row_id:path}") def put_base_price( kind: str, row_id: str, body: BasePriceEdit, session: dict = Depends(verify_session) ) -> JSONResponse: status, payload = base_prices.edit(_kind(kind), row_id, body.values, session.get("user_id")) if status != 200: raise HTTPException(status_code=status, detail=payload["message"]) return JSONResponse(content=payload) @router.get("/overrides") def get_overrides( kind: str = "", state: str = "", page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE ) -> dict: return base_prices.override_items(kind and _kind(kind), state, page=page, size=size) class OverrideClear(BaseModel): kind: str state: Literal["edited", "source_changed", "orphan"] @router.post("/overrides/clear") def clear_overrides(body: OverrideClear, _session: dict = Depends(verify_session)) -> dict: """한 kind 의 그 state 덮개를 한꺼번에 뺌 — kind·state 둘 다 있어야 함(통째로 비우기 막음).""" return {"removed": base_prices.clear_overrides(_kind(body.kind), body.state)}