Files
Gov-chat-bot/backend/app/routers/admin_docs.py
2026-03-26 12:49:43 +09:00

191 lines
5.8 KiB
Python

"""
문서 관리 API — 편집장(editor) 이상 접근.
POST /api/admin/documents/upload — 문서 업로드
POST /api/admin/documents/{id}/approve — 문서 승인 (is_active=True)
GET /api/admin/documents — 문서 목록
DELETE /api/admin/documents/{id} — 문서 삭제
"""
from typing import Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.deps import require_editor, require_admin, get_current_admin
from app.models.admin import AdminUser
from app.models.knowledge import Document
from app.services.document_processor import DocumentProcessor
from app.services.audit import log_action
router = APIRouter(prefix="/api/admin/documents", tags=["admin-documents"])
class DocumentOut(BaseModel):
id: str
filename: str
status: str
is_active: bool
chunk_count: int
version: int
published_at: Optional[datetime] = None
created_at: Optional[datetime] = None
class Config:
from_attributes = True
@router.post("/upload", status_code=status.HTTP_201_CREATED)
async def upload_document(
request: Request,
file: UploadFile = File(...),
published_at: Optional[str] = Form(default=None),
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
"""문서 업로드 → 파싱·임베딩 → VectorDB 저장. is_active=False (승인 대기)."""
tenant_id = current_user.tenant_id
content = await file.read()
# 지원 형식 검사
ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
SUPPORTED = {"txt", "md", "html", "htm", "docx", "pdf"}
if ext not in SUPPORTED:
raise HTTPException(status_code=400, detail=f"Unsupported file type: .{ext}")
# Document 레코드 생성
published = None
if published_at:
try:
published = datetime.fromisoformat(published_at)
except ValueError:
pass
doc = Document(
tenant_id=tenant_id,
filename=file.filename,
source_type="upload",
is_active=False, # 편집장 승인 전
status="pending",
published_at=published,
approved_by=None,
)
db.add(doc)
await db.flush() # id 확보
# 문서 처리
providers = getattr(request.app.state, "providers", {})
processor = DocumentProcessor(
embedding_provider=providers.get("embedding"),
vectordb_provider=providers.get("vectordb"),
db=db,
)
chunk_count = await processor.process(tenant_id, doc, content)
# 감사 로그
await log_action(
db=db,
tenant_id=tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="doc.upload",
target_type="document",
target_id=doc.id,
diff={"filename": file.filename, "chunk_count": chunk_count},
)
return {"id": doc.id, "filename": doc.filename, "status": doc.status, "chunk_count": chunk_count}
@router.post("/{doc_id}/approve")
async def approve_document(
doc_id: str,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
"""문서 승인 → is_active=True."""
tenant_id = current_user.tenant_id
result = await db.execute(
select(Document).where(Document.id == doc_id, Document.tenant_id == tenant_id)
)
doc = result.scalar_one_or_none()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
if doc.status not in ("processed", "embedding_unavailable"):
raise HTTPException(status_code=400, detail=f"Cannot approve document with status: {doc.status}")
doc.is_active = True
doc.approved_by = current_user.id
doc.approved_at = datetime.utcnow()
await db.commit()
await log_action(
db=db,
tenant_id=tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="doc.approve",
target_type="document",
target_id=doc.id,
)
return {"id": doc.id, "is_active": True}
@router.get("", response_model=list[DocumentOut])
async def list_documents(
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(get_current_admin),
):
"""문서 목록 조회."""
result = await db.execute(
select(Document)
.where(Document.tenant_id == current_user.tenant_id)
.order_by(Document.created_at.desc())
)
return result.scalars().all()
@router.delete("/{doc_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_document(
doc_id: str,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
"""문서 삭제 (DB + VectorDB)."""
tenant_id = current_user.tenant_id
result = await db.execute(
select(Document).where(Document.id == doc_id, Document.tenant_id == tenant_id)
)
doc = result.scalar_one_or_none()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
# VectorDB 청크 삭제
providers = getattr(request.app.state, "providers", {})
vectordb = providers.get("vectordb")
if vectordb:
processor = DocumentProcessor(
embedding_provider=providers.get("embedding"),
vectordb_provider=vectordb,
db=db,
)
await processor.delete(tenant_id, doc_id)
await db.delete(doc)
await db.commit()
await log_action(
db=db,
tenant_id=tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="doc.delete",
target_type="document",
target_id=doc_id,
)