"""개인경비 모듈 라우터. - 경로: /expense - 권한: 로그인 + `expense` 모듈 권한 필요 (관리자는 항상 통과). - 데이터: ExpenseStore (JSON). app.state.expense_store 에 연결. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from pydantic import BaseModel, Field from .store import CATEGORIES, METHODS, STATUSES, ExpenseStore router = APIRouter(prefix="/expense", tags=["expense"]) def _store(request: Request) -> ExpenseStore: store = getattr(request.app.state, "expense_store", None) if store is None: raise RuntimeError("ExpenseStore 가 app.state 에 등록되지 않았습니다.") return store def _require_user(request: Request) -> dict[str, Any]: # 순환 import 회피: 핸들러 호출 시점에 main 모듈에서 helper 를 가져온다. from app.main import get_current_user_record # noqa: WPS433 from app.store import has_module # noqa: WPS433 user = get_current_user_record(request) if user is None: raise HTTPException(status_code=401, detail="로그인이 필요합니다.") if not has_module(user, "expense"): raise HTTPException(status_code=403, detail="개인경비 모듈 권한이 없습니다.") return user class ExpensePayload(BaseModel): spent_at: str = Field(..., description="YYYY-MM-DD") category: str method: str merchant: str = "" amount: int = Field(ge=0) memo: str = "" status: str | None = None @router.get("/", response_class=HTMLResponse) async def expense_index(request: Request) -> HTMLResponse: from app.main import ( # noqa: WPS433 build_erp_nav, get_current_user_record, render_template, ) from app.store import has_module, is_admin # noqa: WPS433 user = get_current_user_record(request) if user is None: return RedirectResponse(url="/login", status_code=303) if not has_module(user, "expense"): return render_template( request, "denied.html", {"reason": "개인경비 모듈 접근 권한이 없습니다."}, status_code=403, ) store = _store(request) items = sorted( store.list_for(user["email"]), key=lambda i: (i.get("spent_at", ""), i.get("created_at", "")), reverse=True, ) summary = store.summary_for(user["email"]) return render_template( request, "expense/index.html", { "user": user, "is_admin": is_admin(user), "items": items, "summary": summary, "categories": list(CATEGORIES), "methods": list(METHODS), "statuses": list(STATUSES), "nav_items": build_erp_nav(user, active="expense"), "page_title": "개인경비", "page_subtitle": "법인카드 · 개인지출 · 정산 신청", }, ) @router.get("/api/items") async def list_items( request: Request, user: dict[str, Any] = Depends(_require_user), ) -> JSONResponse: return JSONResponse( { "items": _store(request).list_for(user["email"]), "summary": _store(request).summary_for(user["email"]), } ) @router.post("/api/items") async def create_item( request: Request, body: ExpensePayload, user: dict[str, Any] = Depends(_require_user), ) -> JSONResponse: item = _store(request).create(owner=user["email"], payload=body.model_dump()) return JSONResponse({"item": item}, status_code=201) @router.put("/api/items/{item_id}") async def update_item( request: Request, item_id: str, body: ExpensePayload, user: dict[str, Any] = Depends(_require_user), ) -> JSONResponse: try: item = _store(request).update( item_id=item_id, owner=user["email"], payload=body.model_dump() ) except KeyError: raise HTTPException(status_code=404, detail="항목을 찾을 수 없습니다.") return JSONResponse({"item": item}) @router.delete("/api/items/{item_id}") async def delete_item( request: Request, item_id: str, user: dict[str, Any] = Depends(_require_user), ) -> JSONResponse: try: _store(request).delete(item_id=item_id, owner=user["email"]) except KeyError: raise HTTPException(status_code=404, detail="항목을 찾을 수 없습니다.") return JSONResponse({"ok": True}) @router.get("/health") async def health() -> dict[str, str]: return {"status": "ok", "module": "expense"}