외부 REST API 커넥션 POST/Body + DB 토큰 테스트 지원
This commit is contained in:
@@ -42,6 +42,7 @@ export function AuthenticationConfig({
|
||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
||||
<SelectItem value="db-token">DB 토큰</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -192,6 +193,94 @@ export function AuthenticationConfig({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authType === "db-token" && (
|
||||
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
||||
<h4 className="text-sm font-medium">DB 기반 토큰 설정</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-table-name">테이블명</Label>
|
||||
<Input
|
||||
id="db-table-name"
|
||||
type="text"
|
||||
value={authConfig.dbTableName || ""}
|
||||
onChange={(e) => updateAuthConfig("dbTableName", e.target.value)}
|
||||
placeholder="예: auth_tokens"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-value-column">값 컬럼명</Label>
|
||||
<Input
|
||||
id="db-value-column"
|
||||
type="text"
|
||||
value={authConfig.dbValueColumn || ""}
|
||||
onChange={(e) =>
|
||||
updateAuthConfig("dbValueColumn", e.target.value)
|
||||
}
|
||||
placeholder="예: access_token"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-where-column">조건 컬럼명</Label>
|
||||
<Input
|
||||
id="db-where-column"
|
||||
type="text"
|
||||
value={authConfig.dbWhereColumn || ""}
|
||||
onChange={(e) =>
|
||||
updateAuthConfig("dbWhereColumn", e.target.value)
|
||||
}
|
||||
placeholder="예: service_name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-where-value">조건 값</Label>
|
||||
<Input
|
||||
id="db-where-value"
|
||||
type="text"
|
||||
value={authConfig.dbWhereValue || ""}
|
||||
onChange={(e) =>
|
||||
updateAuthConfig("dbWhereValue", e.target.value)
|
||||
}
|
||||
placeholder="예: kakao"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-header-name">헤더 이름 (선택)</Label>
|
||||
<Input
|
||||
id="db-header-name"
|
||||
type="text"
|
||||
value={authConfig.dbHeaderName || ""}
|
||||
onChange={(e) =>
|
||||
updateAuthConfig("dbHeaderName", e.target.value)
|
||||
}
|
||||
placeholder="기본값: Authorization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="db-header-template">
|
||||
헤더 템플릿 (선택, {{value}} 치환)
|
||||
</Label>
|
||||
<Input
|
||||
id="db-header-template"
|
||||
type="text"
|
||||
value={authConfig.dbHeaderTemplate || ""}
|
||||
onChange={(e) =>
|
||||
updateAuthConfig("dbHeaderTemplate", e.target.value)
|
||||
}
|
||||
placeholder='기본값: "Bearer {{value}}"'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
company_code는 현재 로그인한 사용자의 회사 코드로 자동 필터링됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authType === "none" && (
|
||||
<div className="rounded-md border border-dashed p-4 text-center text-sm text-gray-500">
|
||||
인증이 필요하지 않은 공개 API입니다.
|
||||
|
||||
@@ -21,10 +21,13 @@ import {
|
||||
ExternalRestApiConnection,
|
||||
AuthType,
|
||||
RestApiTestResult,
|
||||
RestApiTestRequest,
|
||||
} from "@/lib/api/externalRestApiConnection";
|
||||
import { HeadersManager } from "./HeadersManager";
|
||||
import { AuthenticationConfig } from "./AuthenticationConfig";
|
||||
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";
|
||||
|
||||
interface RestApiConnectionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -42,6 +45,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [endpointPath, setEndpointPath] = useState("");
|
||||
const [defaultHeaders, setDefaultHeaders] = useState<Record<string, string>>({});
|
||||
const [defaultMethod, setDefaultMethod] = useState("GET");
|
||||
const [defaultBody, setDefaultBody] = useState("");
|
||||
const [authType, setAuthType] = useState<AuthType>("none");
|
||||
const [authConfig, setAuthConfig] = useState<any>({});
|
||||
const [timeout, setTimeout] = useState(30000);
|
||||
@@ -52,6 +57,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
// UI 상태
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [testEndpoint, setTestEndpoint] = useState("");
|
||||
const [testMethod, setTestMethod] = useState("GET");
|
||||
const [testBody, setTestBody] = useState("");
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<RestApiTestResult | null>(null);
|
||||
const [testRequestUrl, setTestRequestUrl] = useState<string>("");
|
||||
@@ -65,12 +72,19 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
setBaseUrl(connection.base_url);
|
||||
setEndpointPath(connection.endpoint_path || "");
|
||||
setDefaultHeaders(connection.default_headers || {});
|
||||
setDefaultMethod(connection.default_method || "GET");
|
||||
setDefaultBody(connection.default_body || "");
|
||||
setAuthType(connection.auth_type);
|
||||
setAuthConfig(connection.auth_config || {});
|
||||
setTimeout(connection.timeout || 30000);
|
||||
setRetryCount(connection.retry_count || 0);
|
||||
setRetryDelay(connection.retry_delay || 1000);
|
||||
setIsActive(connection.is_active === "Y");
|
||||
|
||||
// 테스트 초기값 설정
|
||||
setTestEndpoint("");
|
||||
setTestMethod(connection.default_method || "GET");
|
||||
setTestBody(connection.default_body || "");
|
||||
} else {
|
||||
// 초기화
|
||||
setConnectionName("");
|
||||
@@ -78,16 +92,22 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
setBaseUrl("");
|
||||
setEndpointPath("");
|
||||
setDefaultHeaders({ "Content-Type": "application/json" });
|
||||
setDefaultMethod("GET");
|
||||
setDefaultBody("");
|
||||
setAuthType("none");
|
||||
setAuthConfig({});
|
||||
setTimeout(30000);
|
||||
setRetryCount(0);
|
||||
setRetryDelay(1000);
|
||||
setIsActive(true);
|
||||
|
||||
// 테스트 초기값 설정
|
||||
setTestEndpoint("");
|
||||
setTestMethod("GET");
|
||||
setTestBody("");
|
||||
}
|
||||
|
||||
setTestResult(null);
|
||||
setTestEndpoint("");
|
||||
setTestRequestUrl("");
|
||||
}, [connection, isOpen]);
|
||||
|
||||
@@ -111,14 +131,18 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
setTestRequestUrl(fullUrl);
|
||||
|
||||
try {
|
||||
const result = await ExternalRestApiConnectionAPI.testConnection({
|
||||
const testRequest: RestApiTestRequest = {
|
||||
base_url: baseUrl,
|
||||
endpoint: testEndpoint || undefined,
|
||||
method: testMethod as any,
|
||||
headers: defaultHeaders,
|
||||
body: testBody ? JSON.parse(testBody) : undefined,
|
||||
auth_type: authType,
|
||||
auth_config: authConfig,
|
||||
timeout,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await ExternalRestApiConnectionAPI.testConnection(testRequest);
|
||||
|
||||
setTestResult(result);
|
||||
|
||||
@@ -178,6 +202,20 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 유효성 검증
|
||||
if (defaultBody && defaultMethod !== "GET" && defaultMethod !== "DELETE") {
|
||||
try {
|
||||
JSON.parse(defaultBody);
|
||||
} catch {
|
||||
toast({
|
||||
title: "입력 오류",
|
||||
description: "기본 Body가 올바른 JSON 형식이 아닙니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
@@ -187,6 +225,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
base_url: baseUrl,
|
||||
endpoint_path: endpointPath || undefined,
|
||||
default_headers: defaultHeaders,
|
||||
default_method: defaultMethod,
|
||||
default_body: defaultBody || undefined,
|
||||
auth_type: authType,
|
||||
auth_config: authType === "none" ? undefined : authConfig,
|
||||
timeout,
|
||||
@@ -262,12 +302,28 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
<Label htmlFor="base-url">
|
||||
기본 URL <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="base-url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Select value={defaultMethod} onValueChange={setDefaultMethod}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="Method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="base-url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
도메인 부분만 입력하세요 (예: https://apihub.kma.go.kr)
|
||||
</p>
|
||||
@@ -286,6 +342,21 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 기본 Body (POST, PUT, PATCH일 때만 표시) */}
|
||||
{(defaultMethod === "POST" || defaultMethod === "PUT" || defaultMethod === "PATCH") && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-body">기본 Request Body (JSON)</Label>
|
||||
<Textarea
|
||||
id="default-body"
|
||||
value={defaultBody}
|
||||
onChange={(e) => setDefaultBody(e.target.value)}
|
||||
placeholder='{"key": "value"}'
|
||||
className="font-mono text-xs"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="is-active" checked={isActive} onCheckedChange={setIsActive} />
|
||||
<Label htmlFor="is-active" className="cursor-pointer">
|
||||
@@ -370,13 +441,45 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
<h3 className="text-sm font-semibold">연결 테스트</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-endpoint">테스트 엔드포인트 (선택)</Label>
|
||||
<Input
|
||||
id="test-endpoint"
|
||||
value={testEndpoint}
|
||||
onChange={(e) => setTestEndpoint(e.target.value)}
|
||||
placeholder="엔드포인트 또는 빈칸(기본 URL만 테스트)"
|
||||
/>
|
||||
<Label htmlFor="test-endpoint">테스트 설정</Label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Select value={testMethod} onValueChange={setTestMethod}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="Method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="test-endpoint"
|
||||
value={testEndpoint}
|
||||
onChange={(e) => setTestEndpoint(e.target.value)}
|
||||
placeholder="엔드포인트 (예: /users/1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(testMethod === "POST" || testMethod === "PUT" || testMethod === "PATCH") && (
|
||||
<div className="mt-2">
|
||||
<Label htmlFor="test-body" className="text-xs text-muted-foreground mb-1 block">
|
||||
Test Request Body (JSON)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="test-body"
|
||||
value={testBody}
|
||||
onChange={(e) => setTestBody(e.target.value)}
|
||||
placeholder='{"test": "data"}'
|
||||
className="font-mono text-xs"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="button" variant="outline" onClick={handleTest} disabled={testing}>
|
||||
@@ -388,10 +491,22 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||
{testRequestUrl && (
|
||||
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1 text-xs font-medium">테스트 요청 URL</div>
|
||||
<code className="text-foreground block text-xs break-all">GET {testRequestUrl}</code>
|
||||
<div className="text-muted-foreground mb-1 text-xs font-medium">테스트 요청</div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="outline">{testMethod}</Badge>
|
||||
<code className="text-foreground text-xs break-all">{testRequestUrl}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testBody && (testMethod === "POST" || testMethod === "PUT" || testMethod === "PATCH") && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1 text-xs font-medium">Request Body</div>
|
||||
<pre className="bg-muted p-2 rounded text-xs overflow-auto max-h-[100px]">
|
||||
{testBody}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(defaultHeaders).length > 0 && (
|
||||
<div>
|
||||
<div className="text-muted-foreground mb-1 text-xs font-medium">요청 헤더</div>
|
||||
|
||||
Reference in New Issue
Block a user