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

190 lines
5.3 KiB
Python

"""
FAQ CRUD API — 편집장(editor) 이상.
POST /api/admin/faqs — FAQ 생성
GET /api/admin/faqs — FAQ 목록
PUT /api/admin/faqs/{id} — FAQ 수정
DELETE /api/admin/faqs/{id} — FAQ 삭제
POST /api/admin/faqs/{id}/index — FAQ 벡터 색인
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, 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, get_current_admin
from app.models.admin import AdminUser
from app.models.knowledge import FAQ
from app.services.audit import log_action
from app.services.faq_search import FAQSearchService
router = APIRouter(prefix="/api/admin/faqs", tags=["admin-faq"])
class FAQCreate(BaseModel):
category: Optional[str] = None
question: str
answer: str
keywords: Optional[list[str]] = None
class FAQUpdate(BaseModel):
category: Optional[str] = None
question: Optional[str] = None
answer: Optional[str] = None
keywords: Optional[list[str]] = None
is_active: Optional[bool] = None
class FAQOut(BaseModel):
id: str
category: Optional[str] = None
question: str
answer: str
keywords: Optional[list] = None
hit_count: int
is_active: bool
class Config:
from_attributes = True
@router.post("", status_code=status.HTTP_201_CREATED, response_model=FAQOut)
async def create_faq(
body: FAQCreate,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
faq = FAQ(
tenant_id=current_user.tenant_id,
category=body.category,
question=body.question,
answer=body.answer,
keywords=body.keywords,
created_by=current_user.id,
is_active=True,
)
db.add(faq)
await db.flush()
# 벡터 색인
providers = getattr(request.app.state, "providers", {})
if providers.get("embedding") and providers.get("vectordb"):
service = FAQSearchService(providers["embedding"], providers["vectordb"], db)
try:
await service.index_faq(current_user.tenant_id, faq)
except Exception:
pass # 색인 실패해도 FAQ 저장은 성공
await db.commit()
await db.refresh(faq)
await log_action(
db=db,
tenant_id=current_user.tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="faq.create",
target_type="faq",
target_id=faq.id,
diff={"question": body.question},
)
return faq
@router.get("", response_model=list[FAQOut])
async def list_faqs(
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(get_current_admin),
):
result = await db.execute(
select(FAQ)
.where(FAQ.tenant_id == current_user.tenant_id)
.order_by(FAQ.created_at.desc())
)
return result.scalars().all()
@router.put("/{faq_id}", response_model=FAQOut)
async def update_faq(
faq_id: str,
body: FAQUpdate,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
result = await db.execute(
select(FAQ).where(FAQ.id == faq_id, FAQ.tenant_id == current_user.tenant_id)
)
faq = result.scalar_one_or_none()
if not faq:
raise HTTPException(status_code=404, detail="FAQ not found")
diff = {}
if body.question is not None:
diff["question"] = {"before": faq.question, "after": body.question}
faq.question = body.question
if body.answer is not None:
diff["answer"] = "updated"
faq.answer = body.answer
if body.category is not None:
faq.category = body.category
if body.keywords is not None:
faq.keywords = body.keywords
if body.is_active is not None:
faq.is_active = body.is_active
# 재색인
providers = getattr(request.app.state, "providers", {})
if providers.get("embedding") and providers.get("vectordb") and faq.is_active:
service = FAQSearchService(providers["embedding"], providers["vectordb"], db)
try:
await service.index_faq(current_user.tenant_id, faq)
except Exception:
pass
await db.commit()
await db.refresh(faq)
await log_action(
db=db,
tenant_id=current_user.tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="faq.update",
target_type="faq",
target_id=faq_id,
diff=diff,
)
return faq
@router.delete("/{faq_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_faq(
faq_id: str,
db: AsyncSession = Depends(get_db),
current_user: AdminUser = Depends(require_editor),
):
result = await db.execute(
select(FAQ).where(FAQ.id == faq_id, FAQ.tenant_id == current_user.tenant_id)
)
faq = result.scalar_one_or_none()
if not faq:
raise HTTPException(status_code=404, detail="FAQ not found")
await db.delete(faq)
await db.commit()
await log_action(
db=db,
tenant_id=current_user.tenant_id,
actor_id=current_user.id,
actor_type="admin_user",
action="faq.delete",
target_type="faq",
target_id=faq_id,
)