181 lines
7.6 KiB
TypeScript
181 lines
7.6 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|