191 lines
7.8 KiB
TypeScript
191 lines
7.8 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { getAiAssistantAuth, aiAssistantApi } from "@/lib/api/aiAssistant";
|
||
|
|
import type { UsageSummary, ApiKeyItem, AdminStats } from "@/lib/api/aiAssistant";
|
||
|
|
import { BarChart3, Key, Zap, TrendingUp, Loader2, AlertCircle, Users, Cpu } from "lucide-react";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { Progress } from "@/components/ui/progress";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
export default function AiAssistantDashboardPage() {
|
||
|
|
const auth = getAiAssistantAuth();
|
||
|
|
const user = auth?.user;
|
||
|
|
const isAdmin = user?.role === "admin";
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [usage, setUsage] = useState<UsageSummary | null>(null);
|
||
|
|
const [apiKeys, setApiKeys] = useState<ApiKeyItem[]>([]);
|
||
|
|
const [stats, setStats] = useState<AdminStats | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadData();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const loadData = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const usageRes = await aiAssistantApi.get("/usage");
|
||
|
|
setUsage(usageRes.data?.data ?? null);
|
||
|
|
const keysRes = await aiAssistantApi.get("/api-keys");
|
||
|
|
setApiKeys(keysRes.data?.data ?? []);
|
||
|
|
if (isAdmin) {
|
||
|
|
const statsRes = await aiAssistantApi.get("/admin/stats");
|
||
|
|
setStats(statsRes.data?.data ?? null);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
toast.error("데이터를 불러오는데 실패했습니다.");
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="flex h-64 items-center justify-center">
|
||
|
|
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const monthlyTokens = usage?.usage?.monthly?.totalTokens ?? 0;
|
||
|
|
const monthlyLimit = usage?.limit?.monthly ?? 0;
|
||
|
|
const usagePercent = monthlyLimit > 0 ? Math.round((monthlyTokens / monthlyLimit) * 100) : 0;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">대시보드</h1>
|
||
|
|
<p className="text-muted-foreground mt-1">안녕하세요, {user?.name || user?.email}님!</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">오늘 사용량</CardTitle>
|
||
|
|
<Zap className="text-muted-foreground h-4 w-4" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">
|
||
|
|
{(usage?.usage?.today?.tokens ?? 0).toLocaleString()}
|
||
|
|
</div>
|
||
|
|
<p className="text-muted-foreground text-xs">토큰</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">이번 달 사용량</CardTitle>
|
||
|
|
<BarChart3 className="text-muted-foreground h-4 w-4" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">{monthlyTokens.toLocaleString()}</div>
|
||
|
|
<p className="text-muted-foreground mb-2 text-xs">
|
||
|
|
/ {monthlyLimit.toLocaleString()} 토큰
|
||
|
|
</p>
|
||
|
|
<Progress value={usagePercent} className="h-2" />
|
||
|
|
<p className="text-muted-foreground mt-1 text-right text-xs">{usagePercent}% 사용</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">오늘 요청 수</CardTitle>
|
||
|
|
<TrendingUp className="text-muted-foreground h-4 w-4" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">
|
||
|
|
{(usage?.usage?.today?.requests ?? 0).toLocaleString()}
|
||
|
|
</div>
|
||
|
|
<p className="text-muted-foreground text-xs">회</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">활성 API 키</CardTitle>
|
||
|
|
<Key className="text-muted-foreground h-4 w-4" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">
|
||
|
|
{apiKeys.filter((k) => k.status === "active").length}
|
||
|
|
</div>
|
||
|
|
<p className="text-muted-foreground text-xs">개</p>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isAdmin && stats && (
|
||
|
|
<Card className="bg-gradient-to-r from-primary to-primary/80 text-primary-foreground">
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>시스템 현황</CardTitle>
|
||
|
|
<CardDescription className="text-primary-foreground/70">전체 시스템 통계</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Users className="h-4 w-4" />
|
||
|
|
<span className="text-sm opacity-80">전체 사용자</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-2xl font-bold">{stats.users?.total ?? 0}</p>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Users className="h-4 w-4" />
|
||
|
|
<span className="text-sm opacity-80">활성 사용자</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-2xl font-bold">{stats.users?.active ?? 0}</p>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Key className="h-4 w-4" />
|
||
|
|
<span className="text-sm opacity-80">전체 API 키</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-2xl font-bold">{stats.apiKeys?.total ?? 0}</p>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Cpu className="h-4 w-4" />
|
||
|
|
<span className="text-sm opacity-80">활성 프로바이더</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-2xl font-bold">{stats.providers?.active ?? 0}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>내 API 키</CardTitle>
|
||
|
|
<CardDescription>발급받은 API 키 목록</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{apiKeys.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||
|
|
<AlertCircle className="text-muted-foreground mb-3 h-10 w-10" />
|
||
|
|
<p className="text-muted-foreground">API 키가 없습니다.</p>
|
||
|
|
<p className="text-muted-foreground text-sm">새 키를 발급받으세요.</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-3">
|
||
|
|
{apiKeys.slice(0, 5).map((key) => (
|
||
|
|
<div
|
||
|
|
key={key.id}
|
||
|
|
className="bg-card flex items-center justify-between rounded-lg border p-3"
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
<p className="font-medium">{key.name}</p>
|
||
|
|
<p className="text-muted-foreground font-mono text-sm">{key.keyPrefix}...</p>
|
||
|
|
</div>
|
||
|
|
<Badge variant={key.status === "active" ? "success" : "secondary"}>
|
||
|
|
{key.status === "active" ? "활성" : "비활성"}
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|