파일을 물리적으로 이동하지 않으므로 file_path로 미분류 판단 불가. ai_domain이 NULL 또는 빈 문자열인 문서를 미분류로 취급. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
138 lines
3.8 KiB
Python
138 lines
3.8 KiB
Python
"""대시보드 위젯 데이터 API"""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import func, select, text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.auth import get_current_user
|
|
from core.database import get_session
|
|
from models.document import Document
|
|
from models.queue import ProcessingQueue
|
|
from models.user import User
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class DomainCount(BaseModel):
|
|
domain: str | None
|
|
count: int
|
|
|
|
|
|
class RecentDocument(BaseModel):
|
|
id: int
|
|
title: str | None
|
|
file_format: str
|
|
ai_domain: str | None
|
|
created_at: str
|
|
|
|
|
|
class PipelineStatus(BaseModel):
|
|
stage: str
|
|
status: str
|
|
count: int
|
|
|
|
|
|
class DashboardResponse(BaseModel):
|
|
today_added: int
|
|
today_by_domain: list[DomainCount]
|
|
inbox_count: int
|
|
law_alerts: int
|
|
recent_documents: list[RecentDocument]
|
|
pipeline_status: list[PipelineStatus]
|
|
failed_count: int
|
|
total_documents: int
|
|
|
|
|
|
@router.get("/", response_model=DashboardResponse)
|
|
async def get_dashboard(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
session: Annotated[AsyncSession, Depends(get_session)],
|
|
):
|
|
"""대시보드 위젯 데이터 집계"""
|
|
|
|
# 오늘 추가된 문서
|
|
today_result = await session.execute(
|
|
select(Document.ai_domain, func.count(Document.id))
|
|
.where(func.date(Document.created_at) == func.current_date())
|
|
.group_by(Document.ai_domain)
|
|
)
|
|
today_rows = today_result.all()
|
|
today_added = sum(row[1] for row in today_rows)
|
|
|
|
# Inbox 미분류 수 (ai_domain이 없는 문서 = 미분류)
|
|
inbox_result = await session.execute(
|
|
select(func.count(Document.id))
|
|
.where(
|
|
(Document.ai_domain == None) | (Document.ai_domain == "")
|
|
)
|
|
)
|
|
inbox_count = inbox_result.scalar() or 0
|
|
|
|
# 법령 알림 (오늘)
|
|
law_result = await session.execute(
|
|
select(func.count(Document.id))
|
|
.where(
|
|
Document.source_channel == "law_monitor",
|
|
func.date(Document.created_at) == func.current_date(),
|
|
)
|
|
)
|
|
law_alerts = law_result.scalar() or 0
|
|
|
|
# 최근 문서 5건
|
|
recent_result = await session.execute(
|
|
select(Document)
|
|
.order_by(Document.created_at.desc())
|
|
.limit(5)
|
|
)
|
|
recent_docs = recent_result.scalars().all()
|
|
|
|
# 파이프라인 상태 (24h)
|
|
pipeline_result = await session.execute(
|
|
text("""
|
|
SELECT stage, status, COUNT(*)
|
|
FROM processing_queue
|
|
WHERE created_at > NOW() - INTERVAL '24 hours'
|
|
GROUP BY stage, status
|
|
""")
|
|
)
|
|
|
|
# 실패 건수
|
|
failed_result = await session.execute(
|
|
select(func.count())
|
|
.select_from(ProcessingQueue)
|
|
.where(ProcessingQueue.status == "failed")
|
|
)
|
|
failed_count = failed_result.scalar() or 0
|
|
|
|
# 전체 문서 수
|
|
total_result = await session.execute(select(func.count(Document.id)))
|
|
total_documents = total_result.scalar() or 0
|
|
|
|
return DashboardResponse(
|
|
today_added=today_added,
|
|
today_by_domain=[
|
|
DomainCount(domain=row[0], count=row[1]) for row in today_rows
|
|
],
|
|
inbox_count=inbox_count,
|
|
law_alerts=law_alerts,
|
|
recent_documents=[
|
|
RecentDocument(
|
|
id=doc.id,
|
|
title=doc.title,
|
|
file_format=doc.file_format,
|
|
ai_domain=doc.ai_domain,
|
|
created_at=doc.created_at.isoformat() if doc.created_at else "",
|
|
)
|
|
for doc in recent_docs
|
|
],
|
|
pipeline_status=[
|
|
PipelineStatus(stage=row[0], status=row[1], count=row[2])
|
|
for row in pipeline_result
|
|
],
|
|
failed_count=failed_count,
|
|
total_documents=total_documents,
|
|
)
|