Initial commit: import from sinmb79/Gov-chat-bot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
문서 관리 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,
|
||||
)
|
||||
Reference in New Issue
Block a user