190 lines
5.3 KiB
Python
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,
|
|
)
|