196 lines
7.6 KiB
TypeScript
196 lines
7.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { aiAssistantApi } from "@/lib/api/aiAssistant";
|
||
|
|
import type { UsageSummary } from "@/lib/api/aiAssistant";
|
||
|
|
import { BarChart3, Calendar, Loader2, TrendingUp, Zap, DollarSign } from "lucide-react";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Progress } from "@/components/ui/progress";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
interface DailyUsageItem {
|
||
|
|
date?: string;
|
||
|
|
totalTokens?: number;
|
||
|
|
requestCount?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function AiAssistantUsagePage() {
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [usage, setUsage] = useState<UsageSummary | null>(null);
|
||
|
|
const [dailyUsage, setDailyUsage] = useState<DailyUsageItem[]>([]);
|
||
|
|
const [period, setPeriod] = useState("7");
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadUsage();
|
||
|
|
}, [period]);
|
||
|
|
|
||
|
|
const loadUsage = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const [usageRes, dailyRes] = await Promise.all([
|
||
|
|
aiAssistantApi.get("/usage"),
|
||
|
|
aiAssistantApi.get(`/usage/daily?days=${period}`),
|
||
|
|
]);
|
||
|
|
setUsage(usageRes.data?.data ?? null);
|
||
|
|
setDailyUsage((dailyRes.data?.data as { usage?: DailyUsageItem[] })?.usage ?? []);
|
||
|
|
} 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 todayTokens = usage?.usage?.today?.tokens ?? 0;
|
||
|
|
const todayRequests = usage?.usage?.today?.requests ?? 0;
|
||
|
|
const monthlyTokens = usage?.usage?.monthly?.totalTokens ?? 0;
|
||
|
|
const monthlyCost = usage?.usage?.monthly?.totalCost ?? 0;
|
||
|
|
const monthlyLimit = usage?.limit?.monthly ?? 0;
|
||
|
|
const usagePercent = monthlyLimit > 0 ? Math.round((monthlyTokens / monthlyLimit) * 100) : 0;
|
||
|
|
const maxTokens = Math.max(...dailyUsage.map((d) => d.totalTokens ?? 0), 1);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">사용량</h1>
|
||
|
|
<p className="text-muted-foreground mt-1">API 사용량 및 비용을 확인합니다.</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">{todayTokens.toLocaleString()}</div>
|
||
|
|
</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">{todayRequests.toLocaleString()}</div>
|
||
|
|
</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>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
|
|
<CardTitle className="text-sm font-medium">이번 달 비용</CardTitle>
|
||
|
|
<DollarSign className="text-muted-foreground h-4 w-4" />
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="text-2xl font-bold">${monthlyCost.toFixed(4)}</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<CardTitle>일별 사용량</CardTitle>
|
||
|
|
<CardDescription>기간별 토큰 사용량 추이</CardDescription>
|
||
|
|
</div>
|
||
|
|
<Select value={period} onValueChange={setPeriod}>
|
||
|
|
<SelectTrigger className="w-[140px]">
|
||
|
|
<Calendar className="mr-2 h-4 w-4" />
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="7">최근 7일</SelectItem>
|
||
|
|
<SelectItem value="14">최근 14일</SelectItem>
|
||
|
|
<SelectItem value="30">최근 30일</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
{dailyUsage.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||
|
|
<BarChart3 className="text-muted-foreground mb-4 h-12 w-12" />
|
||
|
|
<p className="text-muted-foreground">사용 기록이 없습니다.</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-3">
|
||
|
|
{dailyUsage.map((day, idx) => (
|
||
|
|
<div key={day.date ?? idx} className="flex items-center gap-4">
|
||
|
|
<div className="text-muted-foreground w-20 text-sm">
|
||
|
|
{day.date
|
||
|
|
? new Date(day.date).toLocaleDateString("ko-KR", {
|
||
|
|
month: "short",
|
||
|
|
day: "numeric",
|
||
|
|
})
|
||
|
|
: "-"}
|
||
|
|
</div>
|
||
|
|
<div className="flex-1">
|
||
|
|
<div className="bg-muted h-8 overflow-hidden rounded-lg">
|
||
|
|
<div
|
||
|
|
className="bg-primary h-full rounded-lg transition-all duration-500"
|
||
|
|
style={{
|
||
|
|
width: `${((day.totalTokens ?? 0) / maxTokens) * 100}%`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="w-28 text-right">
|
||
|
|
<span className="text-sm font-medium">
|
||
|
|
{(day.totalTokens ?? 0).toLocaleString()}
|
||
|
|
</span>
|
||
|
|
<span className="text-muted-foreground ml-1 text-xs">토큰</span>
|
||
|
|
</div>
|
||
|
|
<div className="text-muted-foreground w-16 text-right text-sm">
|
||
|
|
{day.requestCount ?? 0}회
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card className="bg-gradient-to-r from-primary to-primary/80 text-primary-foreground">
|
||
|
|
<CardContent className="pt-6">
|
||
|
|
<div className="mb-4 flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-semibold">
|
||
|
|
현재 플랜: {(usage?.plan ?? "FREE").toUpperCase()}
|
||
|
|
</h3>
|
||
|
|
<p className="text-primary-foreground/70">
|
||
|
|
월간 한도: {monthlyLimit > 0 ? monthlyLimit.toLocaleString() : "무제한"} 토큰
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="text-right">
|
||
|
|
<p className="text-3xl font-bold">{usagePercent}%</p>
|
||
|
|
<p className="text-primary-foreground/70 text-sm">사용률</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Progress value={usagePercent} className="bg-primary-foreground/20 h-3" />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|