rest api 연결 ui 개선

This commit is contained in:
dohyeons 2025-10-27 09:39:11 +09:00
parent 4e3dbd4bc8
commit ef5b86cc4c
3 changed files with 167 additions and 109 deletions

View File

@ -73,6 +73,9 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
// 연결 정보가 변경될 때 폼 데이터 업데이트
useEffect(() => {
// 테스트 관련 상태 초기화
setTestResult(null);
if (connection) {
setFormData({
...connection,
@ -304,7 +307,9 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] max-w-[95vw] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">{isEditMode ? "연결 정보 수정" : "새 외부 DB 연결 추가"}</DialogTitle>
<DialogTitle className="text-base sm:text-lg">
{isEditMode ? "연결 정보 수정" : "새 외부 DB 연결 추가"}
</DialogTitle>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
@ -437,7 +442,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
@ -464,7 +469,7 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
>
{testingConnection ? "테스트 중..." : "연결 테스트"}
</Button>
{testingConnection && <div className="text-sm text-gray-500"> ...</div>}
{testingConnection && <div className="text-muted-foreground text-sm"> ...</div>}
</div>
{/* 테스트 결과 표시 */}
@ -492,7 +497,9 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
{!testResult.success && testResult.error && (
<div className="mt-2 text-xs">
<div> : {testResult.error.code}</div>
{testResult.error.details && <div className="mt-1 text-destructive">{testResult.error.details}</div>}
{testResult.error.details && (
<div className="text-destructive mt-1">{testResult.error.details}</div>
)}
</div>
)}
</div>
@ -602,7 +609,11 @@ export const ExternalDbConnectionModal: React.FC<ExternalDbConnectionModalProps>
>
</Button>
<Button onClick={handleSave} disabled={loading} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
<Button
onClick={handleSave}
disabled={loading}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{loading ? "저장 중..." : isEditMode ? "수정" : "생성"}
</Button>
</DialogFooter>

View File

@ -206,7 +206,7 @@ export function RestApiConnectionList() {
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* 검색 */}
<div className="relative w-full sm:w-[300px]">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder="연결명 또는 URL로 검색..."
value={searchTerm}
@ -246,118 +246,125 @@ export function RestApiConnectionList() {
{/* 추가 버튼 */}
<Button onClick={handleAddConnection} className="h-10 gap-2 text-sm font-medium">
<Plus className="h-4 w-4" />
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 연결 목록 */}
{loading ? (
<div className="flex h-64 items-center justify-center rounded-lg border bg-card shadow-sm">
<div className="text-sm text-muted-foreground"> ...</div>
<div className="bg-card flex h-64 items-center justify-center rounded-lg border shadow-sm">
<div className="text-muted-foreground text-sm"> ...</div>
</div>
) : connections.length === 0 ? (
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border shadow-sm">
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-sm text-muted-foreground"> REST API </p>
<p className="text-muted-foreground text-sm"> REST API </p>
</div>
</div>
) : (
<div className="rounded-lg border bg-card shadow-sm">
<div className="bg-card rounded-lg border shadow-sm">
<Table>
<TableHeader>
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"> URL</TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-right text-sm font-semibold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{connections.map((connection) => (
<TableRow key={connection.id} className="border-b transition-colors hover:bg-muted/50">
<TableCell className="h-16 text-sm">
<div className="font-medium">{connection.connection_name}</div>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50 border-b">
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"> URL</TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"></TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-sm font-semibold"> </TableHead>
<TableHead className="h-12 text-right text-sm font-semibold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{connections.map((connection) => (
<TableRow key={connection.id} className="hover:bg-muted/50 border-b transition-colors">
<TableCell className="h-16 text-sm">
<div className="max-w-[200px]">
<div className="truncate font-medium" title={connection.connection_name}>
{connection.connection_name}
</div>
{connection.description && (
<div className="mt-1 text-xs text-muted-foreground">{connection.description}</div>
)}
</TableCell>
<TableCell className="h-16 font-mono text-sm">{connection.base_url}</TableCell>
<TableCell className="h-16 text-sm">
<Badge variant="outline">
{AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type}
</Badge>
</TableCell>
<TableCell className="h-16 text-center text-sm">
{Object.keys(connection.default_headers || {}).length}
</TableCell>
<TableCell className="h-16 text-sm">
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"}>
{connection.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</TableCell>
<TableCell className="h-16 text-sm">
{connection.last_test_date ? (
<div>
<div>{new Date(connection.last_test_date).toLocaleDateString()}</div>
<Badge
variant={connection.last_test_result === "Y" ? "default" : "destructive"}
className="mt-1"
>
{connection.last_test_result === "Y" ? "성공" : "실패"}
</Badge>
<div className="text-muted-foreground mt-1 truncate text-xs" title={connection.description}>
{connection.description}
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="h-16 text-sm">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleTestConnection(connection)}
disabled={testingConnections.has(connection.id!)}
className="h-9 text-sm"
</div>
</TableCell>
<TableCell className="h-16 font-mono text-sm">
<div className="max-w-[300px] truncate" title={connection.base_url}>
{connection.base_url}
</div>
</TableCell>
<TableCell className="h-16 text-sm">
<Badge variant="outline">{AUTH_TYPE_LABELS[connection.auth_type] || connection.auth_type}</Badge>
</TableCell>
<TableCell className="h-16 text-center text-sm">
{Object.keys(connection.default_headers || {}).length}
</TableCell>
<TableCell className="h-16 text-sm">
<Badge variant={connection.is_active === "Y" ? "default" : "secondary"}>
{connection.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</TableCell>
<TableCell className="h-16 text-sm">
{connection.last_test_date ? (
<div>
<div>{new Date(connection.last_test_date).toLocaleDateString()}</div>
<Badge
variant={connection.last_test_result === "Y" ? "default" : "destructive"}
className="mt-1"
>
{testingConnections.has(connection.id!) ? "테스트 중..." : "테스트"}
</Button>
{testResults.has(connection.id!) && (
<Badge variant={testResults.get(connection.id!) ? "default" : "destructive"}>
{testResults.get(connection.id!) ? "성공" : "실패"}
</Badge>
)}
{connection.last_test_result === "Y" ? "성공" : "실패"}
</Badge>
</div>
</TableCell>
<TableCell className="h-16 text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditConnection(connection)}
className="h-8 w-8"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteConnection(connection)}
className="h-8 w-8 text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="h-16 text-sm">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleTestConnection(connection)}
disabled={testingConnections.has(connection.id!)}
className="h-9 text-sm"
>
{testingConnections.has(connection.id!) ? "테스트 중..." : "테스트"}
</Button>
{testResults.has(connection.id!) && (
<Badge variant={testResults.get(connection.id!) ? "default" : "destructive"}>
{testResults.get(connection.id!) ? "성공" : "실패"}
</Badge>
)}
</div>
</TableCell>
<TableCell className="h-16 text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditConnection(connection)}
className="h-8 w-8"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteConnection(connection)}
className="text-destructive hover:bg-destructive/10 h-8 w-8"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* 연결 설정 모달 */}
@ -377,8 +384,7 @@ export function RestApiConnectionList() {
<AlertDialogTitle className="text-base sm:text-lg"> </AlertDialogTitle>
<AlertDialogDescription className="text-xs sm:text-sm">
"{connectionToDelete?.connection_name}" ?
<br />
.
<br /> .
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2 sm:gap-0">
@ -390,7 +396,7 @@ export function RestApiConnectionList() {
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeleteConnection}
className="h-8 flex-1 bg-destructive text-xs hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
className="bg-destructive hover:bg-destructive/90 h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</AlertDialogAction>

View File

@ -46,6 +46,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
const [testEndpoint, setTestEndpoint] = useState("");
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<RestApiTestResult | null>(null);
const [testRequestUrl, setTestRequestUrl] = useState<string>("");
const [saving, setSaving] = useState(false);
// 기존 연결 데이터 로드
@ -77,6 +78,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
setTestResult(null);
setTestEndpoint("");
setTestRequestUrl("");
}, [connection, isOpen]);
// 연결 테스트
@ -94,6 +96,10 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
setTesting(true);
setTestResult(null);
// 사용자가 테스트하려는 실제 외부 API URL 설정
const fullUrl = testEndpoint ? `${baseUrl}${testEndpoint}` : baseUrl;
setTestRequestUrl(fullUrl);
try {
const result = await ExternalRestApiConnectionAPI.testConnection({
base_url: baseUrl,
@ -220,7 +226,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
<div className="space-y-2">
<Label htmlFor="connection-name">
<span className="text-red-500">*</span>
<span className="text-destructive">*</span>
</Label>
<Input
id="connection-name"
@ -243,7 +249,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
<div className="space-y-2">
<Label htmlFor="base-url">
URL <span className="text-red-500">*</span>
URL <span className="text-destructive">*</span>
</Label>
<Input
id="base-url"
@ -283,14 +289,14 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="flex items-center space-x-2 text-sm font-semibold hover:text-blue-600"
className="hover:text-primary flex items-center space-x-2 text-sm font-semibold transition-colors"
>
<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="bg-muted space-y-4 rounded-md border p-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="timeout"> (ms)</Label>
@ -342,7 +348,7 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
id="test-endpoint"
value={testEndpoint}
onChange={(e) => setTestEndpoint(e.target.value)}
placeholder="/api/v1/test 또는 빈칸 (기본 URL만 테스트)"
placeholder="엔드포인트 또는 빈칸(기본 URL만 테스트)"
/>
</div>
@ -351,6 +357,41 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
{testing ? "테스트 중..." : "연결 테스트"}
</Button>
{/* 테스트 요청 정보 표시 */}
{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>
)}
{testResult && (
<div
className={`rounded-md border p-4 ${