feat(dashboard): 카운트 분리 — 문서함/메모/뉴스/승인대기
전체 문서 1개 카드를 6개로 분리: 문서함, 메모, 뉴스, 승인대기, 법령알림, 시스템. 단일 FILTER 쿼리로 효율적 카운트. 각 카드 클릭 시 해당 페이지로 이동. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
-3
@@ -44,6 +44,10 @@ class DashboardResponse(BaseModel):
|
||||
pipeline_status: list[PipelineStatus]
|
||||
failed_count: int
|
||||
total_documents: int
|
||||
# 카운트 분리: 문서함(비-note/비-news) / 메모(memo+note) / 뉴스(news)
|
||||
documents_count: int = 0
|
||||
memos_count: int = 0
|
||||
news_count: int = 0
|
||||
|
||||
|
||||
@router.get("/", response_model=DashboardResponse)
|
||||
@@ -108,9 +112,23 @@ async def get_dashboard(
|
||||
)
|
||||
failed_count = failed_result.scalar() or 0
|
||||
|
||||
# 전체 문서 수
|
||||
total_result = await session.execute(select(func.count(Document.id)))
|
||||
total_documents = total_result.scalar() or 0
|
||||
# 전체 문서 수 + 카테고리별 분리 (단일 쿼리)
|
||||
# 문서함: 비-note, 비-news / 메모: memo+note / 뉴스: news 유입 경로 기준
|
||||
count_result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE source_channel != 'news' AND file_type != 'note') AS documents,
|
||||
COUNT(*) FILTER (WHERE source_channel = 'memo' AND file_type = 'note') AS memos,
|
||||
COUNT(*) FILTER (WHERE source_channel = 'news') AS news
|
||||
FROM documents WHERE deleted_at IS NULL
|
||||
""")
|
||||
)
|
||||
counts = count_result.one()
|
||||
total_documents = counts[0]
|
||||
documents_count = counts[1]
|
||||
memos_count = counts[2]
|
||||
news_count = counts[3]
|
||||
|
||||
return DashboardResponse(
|
||||
today_added=today_added,
|
||||
@@ -135,4 +153,7 @@ async def get_dashboard(
|
||||
],
|
||||
failed_count=failed_count,
|
||||
total_documents=total_documents,
|
||||
documents_count=documents_count,
|
||||
memos_count=memos_count,
|
||||
news_count=news_count,
|
||||
)
|
||||
|
||||
@@ -35,6 +35,9 @@ export interface DashboardSummary {
|
||||
pipeline_status: PipelineStatus[];
|
||||
failed_count: number;
|
||||
total_documents: number;
|
||||
documents_count: number;
|
||||
memos_count: number;
|
||||
news_count: number;
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 60_000;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
import FormatIcon from '$lib/components/FormatIcon.svelte';
|
||||
import { Calendar, Inbox, Scale, FileText, Activity } from 'lucide-svelte';
|
||||
import { Calendar, Inbox, Scale, FileText, Activity, StickyNote, Newspaper } from 'lucide-svelte';
|
||||
|
||||
// ─── 파생 값들 ───────────────────────────────────────────────
|
||||
// legacy store + runes 혼합: 템플릿에서 $dashboardSummary로 자동 구독.
|
||||
@@ -175,70 +175,78 @@
|
||||
</div>
|
||||
{:else if summary}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-4">
|
||||
<!-- ─── 행 1: stat 4 ─── -->
|
||||
<div class="lg:col-span-3">
|
||||
<Card>
|
||||
<!-- ─── 행 1: 카테고리별 카운트 (6칸) ─── -->
|
||||
<a href="/documents" class="lg:col-span-2 block">
|
||||
<Card interactive>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">전체 문서</p>
|
||||
<FileText size={18} class="text-faint" />
|
||||
<p class="text-sm text-dim">문서함</p>
|
||||
<FileText size={16} class="text-faint" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold mt-2 text-text">
|
||||
{summary.total_documents.toLocaleString()}
|
||||
<p class="text-2xl font-bold mt-1 text-text">
|
||||
{(summary.documents_count ?? 0).toLocaleString()}
|
||||
</p>
|
||||
<p class="text-xs text-dim mt-1">
|
||||
오늘 +{summary.today_added}
|
||||
<p class="text-xs text-dim mt-0.5">오늘 +{summary.today_added}</p>
|
||||
</Card>
|
||||
</a>
|
||||
|
||||
<a href="/memos" class="lg:col-span-2 block">
|
||||
<Card interactive>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">메모</p>
|
||||
<StickyNote size={16} class="text-faint" />
|
||||
</div>
|
||||
<p class="text-2xl font-bold mt-1 text-text">
|
||||
{(summary.memos_count ?? 0).toLocaleString()}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="lg:col-span-3">
|
||||
<Card>
|
||||
<a href="/news" class="lg:col-span-2 block">
|
||||
<Card interactive>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">Inbox 미분류</p>
|
||||
<Inbox size={18} class="text-faint" />
|
||||
<p class="text-sm text-dim">뉴스</p>
|
||||
<Newspaper size={16} class="text-faint" />
|
||||
</div>
|
||||
<p
|
||||
class="text-3xl font-bold mt-2 {summary.inbox_count > 0
|
||||
? 'text-warning'
|
||||
: 'text-text'}"
|
||||
>
|
||||
<p class="text-2xl font-bold mt-1 text-text">
|
||||
{(summary.news_count ?? 0).toLocaleString()}
|
||||
</p>
|
||||
</Card>
|
||||
</a>
|
||||
|
||||
<a href="/inbox" class="lg:col-span-2 block">
|
||||
<Card interactive>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">승인 대기</p>
|
||||
<Inbox size={16} class="text-faint" />
|
||||
</div>
|
||||
<p class="text-2xl font-bold mt-1 {summary.inbox_count > 0 ? 'text-warning' : 'text-text'}">
|
||||
{summary.inbox_count}
|
||||
</p>
|
||||
{#if summary.inbox_count > 0}
|
||||
<a
|
||||
href="/inbox"
|
||||
class="text-xs text-accent hover:underline mt-1 inline-block"
|
||||
>
|
||||
분류하기 →
|
||||
</a>
|
||||
{:else}
|
||||
<p class="text-xs text-dim mt-1">대기 없음</p>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="lg:col-span-3">
|
||||
<div class="lg:col-span-2">
|
||||
<Card>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">법령 알림</p>
|
||||
<Scale size={18} class="text-faint" />
|
||||
<Scale size={16} class="text-faint" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold mt-2 text-text">{summary.law_alerts}</p>
|
||||
<p class="text-xs text-dim mt-1">오늘 변경</p>
|
||||
<p class="text-2xl font-bold mt-1 text-text">{summary.law_alerts}</p>
|
||||
<p class="text-xs text-dim mt-0.5">오늘 변경</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-3">
|
||||
<div class="lg:col-span-2">
|
||||
<Card>
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-sm text-dim">시스템 상태</p>
|
||||
<Activity size={18} class="text-faint" />
|
||||
<p class="text-sm text-dim">시스템</p>
|
||||
<Activity size={16} class="text-faint" />
|
||||
</div>
|
||||
{#if systemView}
|
||||
<p class="text-3xl font-bold mt-2 {TONE_TEXT[systemView.tone]}">
|
||||
<p class="text-2xl font-bold mt-1 {TONE_TEXT[systemView.tone]}">
|
||||
{systemView.label}
|
||||
</p>
|
||||
<p class="text-xs text-dim mt-1">{systemView.sub}</p>
|
||||
<p class="text-xs text-dim mt-0.5">{systemView.sub}</p>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user