ERP-node/frontend/app/(main)/admin/aiAssistant/api-test/page.tsx

181 lines
7.6 KiB
TypeScript
Raw Normal View History

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
const DEFAULT_BASE = "http://localhost:3100/api/v1";
const PRESETS = [
{ name: "채팅 완성", method: "POST", endpoint: "/chat/completions", body: '{"model":"gemini-2.0-flash","messages":[{"role":"user","content":"안녕하세요!"}],"temperature":0.7}' },
{ name: "모델 목록", method: "GET", endpoint: "/models", body: "" },
{ name: "사용량", method: "GET", endpoint: "/usage", body: "" },
{ name: "API 키 목록", method: "GET", endpoint: "/api-keys", body: "" },
];
export default function AiAssistantApiTestPage() {
const [baseUrl, setBaseUrl] = useState(
typeof window !== "undefined" ? (process.env.NEXT_PUBLIC_AI_ASSISTANT_API_URL || DEFAULT_BASE) : DEFAULT_BASE
);
const [apiKey, setApiKey] = useState("");
const [method, setMethod] = useState("POST");
const [endpoint, setEndpoint] = useState("/chat/completions");
const [body, setBody] = useState(PRESETS[0].body);
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState<{ status: number; statusText: string; data: unknown } | null>(null);
const [responseTime, setResponseTime] = useState<number | null>(null);
const [copied, setCopied] = useState(false);
const apply = (p: (typeof PRESETS)[0]) => {
setMethod(p.method);
setEndpoint(p.endpoint);
setBody(p.body);
};
const send = async () => {
setLoading(true);
setResponse(null);
setResponseTime(null);
const start = Date.now();
try {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const opt: RequestInit = { method, headers };
if (method !== "GET" && body.trim()) {
try {
JSON.parse(body);
opt.body = body;
} catch {
toast.error("JSON 형식 오류");
setLoading(false);
return;
}
}
const res = await fetch(`${baseUrl}${endpoint}`, opt);
const elapsed = Date.now() - start;
setResponseTime(elapsed);
const ct = res.headers.get("content-type");
const data = ct?.includes("json") ? await res.json() : await res.text();
setResponse({ status: res.status, statusText: res.statusText, data });
toast.success(res.ok ? `성공 ${res.status}` : `실패 ${res.status}`);
} catch (e) {
setResponseTime(Date.now() - start);
setResponse({ status: 0, statusText: "Network Error", data: { error: String(e) } });
toast.error("네트워크 오류");
} finally {
setLoading(false);
}
};
const copyRes = () => {
navigator.clipboard.writeText(JSON.stringify(response?.data, null, 2));
setCopied(true);
toast.success("복사됨");
setTimeout(() => setCopied(false), 2000);
};
const statusV = (s: number) => (s >= 200 && s < 300 ? "success" : s >= 400 ? "destructive" : "secondary");
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">API </h1>
<p className="text-muted-foreground mt-1">API를 .</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">API </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Base URL</Label>
<Input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} />
</div>
<div className="space-y-2">
<Label>API JWT</Label>
<Input type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-xxx" />
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{PRESETS.map((p, i) => (
<Button key={i} variant="outline" size="sm" onClick={() => apply(p)}>
<Badge variant="secondary" className="mr-2 text-xs">{p.method}</Badge>
{p.name}
</Button>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Select value={method} onValueChange={setMethod}>
<SelectTrigger className="w-[100px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
<Input value={endpoint} onChange={(e) => setEndpoint(e.target.value)} className="flex-1" />
</div>
{method !== "GET" && (
<div className="space-y-2">
<Label>Body (JSON)</Label>
<Textarea value={body} onChange={(e) => setBody(e.target.value)} className="font-mono text-sm min-h-[180px]" />
</div>
)}
<Button className="w-full" onClick={send} disabled={loading}>
{loading ? "요청 중..." : "요청 보내기"}
</Button>
</CardContent>
</Card>
</div>
<Card className="h-full">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"></CardTitle>
{response && (
<div className="flex items-center gap-2">
<Badge variant={statusV(response.status)}>{response.status} {response.statusText}</Badge>
{responseTime != null && <Badge variant="outline">{responseTime}ms</Badge>}
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={copyRes}>
{copied ? "✓" : "복사"}
</Button>
</div>
)}
</div>
</CardHeader>
<CardContent>
{!response ? (
<p className="text-muted-foreground py-12 text-center"> .</p>
) : (
<pre className="bg-muted max-h-[500px] overflow-auto rounded-lg p-4 text-sm font-mono whitespace-pre-wrap">
{typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2)}
</pre>
)}
</CardContent>
</Card>
</div>
</div>
);
}