바코드 업데이트 및 AI LLm 적용용
This commit is contained in:
299
frontend/app/(main)/admin/aiAssistant/api-keys/page.tsx
Normal file
299
frontend/app/(main)/admin/aiAssistant/api-keys/page.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { aiAssistantApi } from "@/lib/api/aiAssistant";
|
||||
import type { ApiKeyItem } from "@/lib/api/aiAssistant";
|
||||
import {
|
||||
Key,
|
||||
Plus,
|
||||
Copy,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Check,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AiAssistantApiKeysPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKeyItem[]>([]);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [newKeyDialogOpen, setNewKeyDialogOpen] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [newKey, setNewKey] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadApiKeys();
|
||||
}, []);
|
||||
|
||||
const loadApiKeys = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await aiAssistantApi.get("/api-keys");
|
||||
setApiKeys(res.data?.data ?? []);
|
||||
} catch {
|
||||
toast.error("API 키 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createApiKey = async () => {
|
||||
if (!newKeyName.trim()) {
|
||||
toast.error("키 이름을 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
try {
|
||||
const res = await aiAssistantApi.post("/api-keys", { name: newKeyName });
|
||||
setNewKey((res.data?.data as { key?: string })?.key ?? "");
|
||||
setCreateDialogOpen(false);
|
||||
setNewKeyDialogOpen(true);
|
||||
setNewKeyName("");
|
||||
loadApiKeys();
|
||||
toast.success("API 키가 생성되었습니다.");
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
err && typeof err === "object" && "response" in err
|
||||
? (err as { response?: { data?: { error?: { message?: string } } } }).response?.data
|
||||
?.error?.message
|
||||
: null;
|
||||
toast.error(msg ?? "API 키 생성에 실패했습니다.");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const revokeApiKey = async (id: number) => {
|
||||
if (!confirm("이 API 키를 폐기하시겠습니까?")) return;
|
||||
try {
|
||||
await aiAssistantApi.delete(`/api-keys/${id}`);
|
||||
loadApiKeys();
|
||||
toast.success("API 키가 폐기되었습니다.");
|
||||
} catch {
|
||||
toast.error("API 키 폐기에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
toast.success("클립보드에 복사되었습니다.");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error("복사에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const baseUrl =
|
||||
typeof window !== "undefined"
|
||||
? process.env.NEXT_PUBLIC_AI_ASSISTANT_API_URL || "http://localhost:3100/api/v1"
|
||||
: "";
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">API 키 관리</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
외부 시스템에서 AI Assistant API를 사용하기 위한 키를 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
새 API 키
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>새 API 키 생성</DialogTitle>
|
||||
<DialogDescription>
|
||||
새로운 API 키를 생성합니다. 키는 한 번만 표시되므로 안전하게 보관하세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="keyName">키 이름</Label>
|
||||
<Input
|
||||
id="keyName"
|
||||
placeholder="예: Production Server"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={createApiKey} disabled={creating}>
|
||||
{creating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
생성
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Dialog open={newKeyDialogOpen} onOpenChange={setNewKeyDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>API 키가 생성되었습니다</DialogTitle>
|
||||
<DialogDescription>
|
||||
이 키는 다시 표시되지 않습니다. 안전한 곳에 복사하여 보관하세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type={showKey ? "text" : "password"}
|
||||
value={newKey}
|
||||
readOnly
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={() => setShowKey(!showKey)}>
|
||||
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={() => copyToClipboard(newKey)}>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setNewKeyDialogOpen(false)}>확인</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API 키 목록</CardTitle>
|
||||
<CardDescription>발급된 모든 API 키를 확인하고 관리합니다.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{apiKeys.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Key className="text-muted-foreground mb-4 h-12 w-12" />
|
||||
<h3 className="text-lg font-medium">API 키가 없습니다</h3>
|
||||
<p className="text-muted-foreground mt-1 text-sm">새 API 키를 생성하여 시작하세요.</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead>키</TableHead>
|
||||
<TableHead>상태</TableHead>
|
||||
<TableHead>사용량</TableHead>
|
||||
<TableHead>마지막 사용</TableHead>
|
||||
<TableHead>생성일</TableHead>
|
||||
<TableHead className="text-right">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((key) => (
|
||||
<TableRow key={key.id}>
|
||||
<TableCell className="font-medium">{key.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">
|
||||
{key.keyPrefix}...
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => copyToClipboard(key.keyPrefix + "...")}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={key.status === "active" ? "success" : "secondary"}>
|
||||
{key.status === "active" ? "활성" : "폐기됨"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{(key.usageCount ?? 0).toLocaleString()} 토큰</TableCell>
|
||||
<TableCell>
|
||||
{key.lastUsedAt
|
||||
? new Date(key.lastUsedAt).toLocaleDateString("ko-KR")
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell>{new Date(key.createdAt).toLocaleDateString("ko-KR")}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{key.status === "active" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive h-8 w-8"
|
||||
onClick={() => revokeApiKey(key.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API 사용 방법</CardTitle>
|
||||
<CardDescription>
|
||||
발급받은 API 키를 Authorization 헤더에 포함하여 요청하세요.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-muted overflow-x-auto rounded-lg p-4 text-sm">
|
||||
{`curl -X POST ${baseUrl}/chat/completions \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-d '{"model": "gemini-2.0-flash", "messages": [{"role": "user", "content": "Hello!"}]}'`}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user