ERP-node/frontend/components/admin/RestApiConnectionModal.tsx

463 lines
16 KiB
TypeScript
Raw Normal View History

2025-10-21 10:59:15 +09:00
"use client";
import React, { useState, useEffect } from "react";
import { X, Save, TestTube, ChevronDown, ChevronUp } from "lucide-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 { Switch } from "@/components/ui/switch";
2025-11-05 16:36:32 +09:00
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
2025-10-21 10:59:15 +09:00
import { useToast } from "@/hooks/use-toast";
import {
ExternalRestApiConnectionAPI,
ExternalRestApiConnection,
AuthType,
RestApiTestResult,
} from "@/lib/api/externalRestApiConnection";
import { HeadersManager } from "./HeadersManager";
import { AuthenticationConfig } from "./AuthenticationConfig";
import { Badge } from "@/components/ui/badge";
interface RestApiConnectionModalProps {
isOpen: boolean;
onClose: () => void;
onSave: () => void;
connection?: ExternalRestApiConnection;
}
export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }: RestApiConnectionModalProps) {
const { toast } = useToast();
// 폼 상태
const [connectionName, setConnectionName] = useState("");
const [description, setDescription] = useState("");
const [baseUrl, setBaseUrl] = useState("");
const [endpointPath, setEndpointPath] = useState("");
2025-10-21 10:59:15 +09:00
const [defaultHeaders, setDefaultHeaders] = useState<Record<string, string>>({});
const [authType, setAuthType] = useState<AuthType>("none");
const [authConfig, setAuthConfig] = useState<any>({});
const [timeout, setTimeout] = useState(30000);
const [retryCount, setRetryCount] = useState(0);
const [retryDelay, setRetryDelay] = useState(1000);
const [isActive, setIsActive] = useState(true);
// UI 상태
const [showAdvanced, setShowAdvanced] = useState(false);
const [testEndpoint, setTestEndpoint] = useState("");
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<RestApiTestResult | null>(null);
2025-10-27 09:39:11 +09:00
const [testRequestUrl, setTestRequestUrl] = useState<string>("");
2025-10-21 10:59:15 +09:00
const [saving, setSaving] = useState(false);
// 기존 연결 데이터 로드
useEffect(() => {
if (connection) {
setConnectionName(connection.connection_name);
setDescription(connection.description || "");
setBaseUrl(connection.base_url);
setEndpointPath(connection.endpoint_path || "");
2025-10-21 10:59:15 +09:00
setDefaultHeaders(connection.default_headers || {});
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");
} else {
// 초기화
setConnectionName("");
setDescription("");
setBaseUrl("");
setEndpointPath("");
2025-10-21 10:59:15 +09:00
setDefaultHeaders({ "Content-Type": "application/json" });
setAuthType("none");
setAuthConfig({});
setTimeout(30000);
setRetryCount(0);
setRetryDelay(1000);
setIsActive(true);
}
setTestResult(null);
setTestEndpoint("");
2025-10-27 09:39:11 +09:00
setTestRequestUrl("");
2025-10-21 10:59:15 +09:00
}, [connection, isOpen]);
// 연결 테스트
const handleTest = async () => {
// 유효성 검증
if (!baseUrl.trim()) {
toast({
title: "입력 오류",
description: "기본 URL을 입력해주세요.",
variant: "destructive",
});
return;
}
setTesting(true);
setTestResult(null);
2025-10-27 09:39:11 +09:00
// 사용자가 테스트하려는 실제 외부 API URL 설정
const fullUrl = testEndpoint ? `${baseUrl}${testEndpoint}` : baseUrl;
setTestRequestUrl(fullUrl);
2025-10-21 10:59:15 +09:00
try {
const result = await ExternalRestApiConnectionAPI.testConnection({
base_url: baseUrl,
endpoint: testEndpoint || undefined,
headers: defaultHeaders,
auth_type: authType,
auth_config: authConfig,
timeout,
});
setTestResult(result);
if (result.success) {
toast({
title: "연결 성공",
description: `응답 시간: ${result.response_time}ms`,
});
} else {
toast({
title: "연결 실패",
description: result.message,
variant: "destructive",
});
}
} catch (error) {
toast({
title: "테스트 오류",
description: error instanceof Error ? error.message : "알 수 없는 오류",
variant: "destructive",
});
} finally {
setTesting(false);
}
};
// 저장
const handleSave = async () => {
// 유효성 검증
if (!connectionName.trim()) {
toast({
title: "입력 오류",
description: "연결명을 입력해주세요.",
variant: "destructive",
});
return;
}
if (!baseUrl.trim()) {
toast({
title: "입력 오류",
description: "기본 URL을 입력해주세요.",
variant: "destructive",
});
return;
}
// URL 형식 검증
try {
new URL(baseUrl);
} catch {
toast({
title: "입력 오류",
description: "올바른 URL 형식이 아닙니다.",
variant: "destructive",
});
return;
}
setSaving(true);
try {
const data: ExternalRestApiConnection = {
connection_name: connectionName,
description: description || undefined,
base_url: baseUrl,
endpoint_path: endpointPath || undefined,
2025-10-21 10:59:15 +09:00
default_headers: defaultHeaders,
auth_type: authType,
auth_config: authType === "none" ? undefined : authConfig,
timeout,
retry_count: retryCount,
retry_delay: retryDelay,
company_code: "*",
is_active: isActive ? "Y" : "N",
};
if (connection?.id) {
await ExternalRestApiConnectionAPI.updateConnection(connection.id, data);
toast({
title: "수정 완료",
description: "연결이 수정되었습니다.",
});
} else {
await ExternalRestApiConnectionAPI.createConnection(data);
toast({
title: "생성 완료",
description: "연결이 생성되었습니다.",
});
}
onSave();
onClose();
} catch (error) {
toast({
title: "저장 실패",
description: error instanceof Error ? error.message : "알 수 없는 오류",
variant: "destructive",
});
} finally {
setSaving(false);
}
};
return (
2025-11-05 16:36:32 +09:00
<ResizableDialog open={isOpen} onOpenChange={onClose}>
<ResizableDialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto">
<ResizableDialogHeader>
<ResizableDialogTitle>{connection ? "REST API 연결 수정" : "새 REST API 연결 추가"}</ResizableDialogTitle>
</ResizableDialogHeader>
2025-10-21 10:59:15 +09:00
<div className="space-y-6 py-4">
{/* 기본 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<div className="space-y-2">
<Label htmlFor="connection-name">
2025-10-27 09:39:11 +09:00
<span className="text-destructive">*</span>
2025-10-21 10:59:15 +09:00
</Label>
<Input
id="connection-name"
value={connectionName}
onChange={(e) => setConnectionName(e.target.value)}
placeholder="예: 날씨 API"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="연결에 대한 설명을 입력하세요"
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="base-url">
2025-10-27 09:39:11 +09:00
URL <span className="text-destructive">*</span>
2025-10-21 10:59:15 +09:00
</Label>
<Input
id="base-url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="https://api.example.com"
/>
<p className="text-muted-foreground text-xs">
(: https://apihub.kma.go.kr)
</p>
</div>
<div className="space-y-2">
<Label htmlFor="endpoint-path"> </Label>
<Input
id="endpoint-path"
value={endpointPath}
onChange={(e) => setEndpointPath(e.target.value)}
placeholder="/api/typ01/url/wrn_now_data.php"
/>
<p className="text-muted-foreground text-xs">
API ()
</p>
2025-10-21 10:59:15 +09:00
</div>
<div className="flex items-center space-x-2">
<Switch id="is-active" checked={isActive} onCheckedChange={setIsActive} />
<Label htmlFor="is-active" className="cursor-pointer">
</Label>
</div>
</div>
{/* 헤더 관리 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<HeadersManager headers={defaultHeaders} onChange={setDefaultHeaders} />
</div>
{/* 인증 설정 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold"> </h3>
<AuthenticationConfig
authType={authType}
authConfig={authConfig}
onAuthTypeChange={setAuthType}
onAuthConfigChange={setAuthConfig}
/>
</div>
{/* 고급 설정 */}
<div className="space-y-4">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
2025-10-27 09:39:11 +09:00
className="hover:text-primary flex items-center space-x-2 text-sm font-semibold transition-colors"
2025-10-21 10:59:15 +09:00
>
<span> </span>
{showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
{showAdvanced && (
2025-10-27 09:39:11 +09:00
<div className="bg-muted space-y-4 rounded-md border p-4">
2025-10-21 10:59:15 +09:00
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="timeout"> (ms)</Label>
<Input
id="timeout"
type="number"
value={timeout}
onChange={(e) => setTimeout(parseInt(e.target.value) || 30000)}
min={1000}
max={120000}
/>
</div>
<div className="space-y-2">
<Label htmlFor="retry-count"> </Label>
<Input
id="retry-count"
type="number"
value={retryCount}
onChange={(e) => setRetryCount(parseInt(e.target.value) || 0)}
min={0}
max={5}
/>
</div>
<div className="space-y-2">
<Label htmlFor="retry-delay"> (ms)</Label>
<Input
id="retry-delay"
type="number"
value={retryDelay}
onChange={(e) => setRetryDelay(parseInt(e.target.value) || 1000)}
min={100}
max={10000}
/>
</div>
</div>
</div>
)}
</div>
{/* 연결 테스트 */}
<div className="space-y-4">
<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)}
2025-10-27 09:39:11 +09:00
placeholder="엔드포인트 또는 빈칸(기본 URL만 테스트)"
2025-10-21 10:59:15 +09:00
/>
</div>
<Button type="button" variant="outline" onClick={handleTest} disabled={testing}>
<TestTube className="mr-2 h-4 w-4" />
{testing ? "테스트 중..." : "연결 테스트"}
</Button>
2025-10-27 09:39:11 +09:00
{/* 테스트 요청 정보 표시 */}
{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>
{Object.keys(defaultHeaders).length > 0 && (
<div>
<div className="text-muted-foreground mb-1 text-xs font-medium"> </div>
<div className="space-y-1">
{Object.entries(defaultHeaders).map(([key, value]) => (
<code key={key} className="text-foreground block text-xs">
{key}: {value}
</code>
))}
</div>
</div>
)}
{authType !== "none" && (
<div>
<div className="text-muted-foreground mb-1 text-xs font-medium"> </div>
<code className="text-foreground block text-xs">
{authType === "api-key" && "API Key"}
{authType === "bearer" && "Bearer Token"}
{authType === "basic" && "Basic Auth"}
{authType === "oauth2" && "OAuth 2.0"}
</code>
</div>
)}
</div>
)}
2025-10-21 10:59:15 +09:00
{testResult && (
<div
className={`rounded-md border p-4 ${
testResult.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"
}`}
>
<div className="mb-2 flex items-center justify-between">
<Badge variant={testResult.success ? "default" : "destructive"}>
{testResult.success ? "성공" : "실패"}
</Badge>
{testResult.response_time && (
<span className="text-sm text-gray-600"> : {testResult.response_time}ms</span>
)}
</div>
<p className="text-sm">{testResult.message}</p>
{testResult.status_code && (
<p className="mt-1 text-xs text-gray-500"> : {testResult.status_code}</p>
)}
{testResult.error_details && <p className="mt-2 text-xs text-red-600">{testResult.error_details}</p>}
</div>
)}
</div>
</div>
2025-11-05 16:36:32 +09:00
<ResizableDialogFooter>
2025-10-21 10:59:15 +09:00
<Button type="button" variant="outline" onClick={onClose}>
<X className="mr-2 h-4 w-4" />
</Button>
<Button type="button" onClick={handleSave} disabled={saving}>
<Save className="mr-2 h-4 w-4" />
{saving ? "저장 중..." : connection ? "수정" : "생성"}
</Button>
2025-11-05 16:36:32 +09:00
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
2025-10-21 10:59:15 +09:00
);
}