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

395 lines
13 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";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
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 [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);
const [saving, setSaving] = useState(false);
// 기존 연결 데이터 로드
useEffect(() => {
if (connection) {
setConnectionName(connection.connection_name);
setDescription(connection.description || "");
setBaseUrl(connection.base_url);
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("");
setDefaultHeaders({ "Content-Type": "application/json" });
setAuthType("none");
setAuthConfig({});
setTimeout(30000);
setRetryCount(0);
setRetryDelay(1000);
setIsActive(true);
}
setTestResult(null);
setTestEndpoint("");
}, [connection, isOpen]);
// 연결 테스트
const handleTest = async () => {
// 유효성 검증
if (!baseUrl.trim()) {
toast({
title: "입력 오류",
description: "기본 URL을 입력해주세요.",
variant: "destructive",
});
return;
}
setTesting(true);
setTestResult(null);
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,
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto">
<DialogHeader>
<DialogTitle>{connection ? "REST API 연결 수정" : "새 REST API 연결 추가"}</DialogTitle>
</DialogHeader>
<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">
<span className="text-red-500">*</span>
</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">
URL <span className="text-red-500">*</span>
</Label>
<Input
id="base-url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="https://api.example.com"
/>
</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)}
className="flex items-center space-x-2 text-sm font-semibold hover:text-blue-600"
>
<span> </span>
{showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</button>
{showAdvanced && (
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
<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)}
placeholder="/api/v1/test 또는 빈칸 (기본 URL만 테스트)"
/>
</div>
<Button type="button" variant="outline" onClick={handleTest} disabled={testing}>
<TestTube className="mr-2 h-4 w-4" />
{testing ? "테스트 중..." : "연결 테스트"}
</Button>
{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>
<DialogFooter>
<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>
</DialogFooter>
</DialogContent>
</Dialog>
);
}