230 lines
7.6 KiB
TypeScript
230 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { ChartDataSource } from "@/components/admin/dashboard/types";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Loader2, CheckCircle, XCircle } from "lucide-react";
|
|
|
|
interface MultiDatabaseConfigProps {
|
|
dataSource: ChartDataSource;
|
|
onChange: (updates: Partial<ChartDataSource>) => void;
|
|
}
|
|
|
|
interface ExternalConnection {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
}
|
|
|
|
export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatabaseConfigProps) {
|
|
const [testing, setTesting] = useState(false);
|
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string; rowCount?: number } | null>(null);
|
|
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
|
const [loadingConnections, setLoadingConnections] = useState(false);
|
|
|
|
// 외부 DB 커넥션 목록 로드
|
|
useEffect(() => {
|
|
if (dataSource.connectionType === "external") {
|
|
loadExternalConnections();
|
|
}
|
|
}, [dataSource.connectionType]);
|
|
|
|
const loadExternalConnections = async () => {
|
|
setLoadingConnections(true);
|
|
try {
|
|
const response = await fetch("/api/admin/reports/external-connections", {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.success && result.data) {
|
|
const connections = Array.isArray(result.data) ? result.data : result.data.data || [];
|
|
setExternalConnections(connections);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("외부 DB 커넥션 로드 실패:", error);
|
|
} finally {
|
|
setLoadingConnections(false);
|
|
}
|
|
};
|
|
|
|
// 쿼리 테스트
|
|
const handleTestQuery = async () => {
|
|
if (!dataSource.query) {
|
|
setTestResult({ success: false, message: "SQL 쿼리를 입력해주세요" });
|
|
return;
|
|
}
|
|
|
|
setTesting(true);
|
|
setTestResult(null);
|
|
|
|
try {
|
|
// dashboardApi 사용 (인증 토큰 자동 포함)
|
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
|
|
|
if (dataSource.connectionType === "external" && dataSource.externalConnectionId) {
|
|
// 외부 DB
|
|
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
|
const result = await ExternalDbConnectionAPI.executeQuery(
|
|
parseInt(dataSource.externalConnectionId),
|
|
dataSource.query
|
|
);
|
|
|
|
if (result.success && result.data) {
|
|
const rowCount = Array.isArray(result.data.rows) ? result.data.rows.length : 0;
|
|
setTestResult({
|
|
success: true,
|
|
message: "쿼리 실행 성공",
|
|
rowCount,
|
|
});
|
|
} else {
|
|
setTestResult({ success: false, message: result.message || "쿼리 실행 실패" });
|
|
}
|
|
} else {
|
|
// 현재 DB
|
|
const result = await dashboardApi.executeQuery(dataSource.query);
|
|
setTestResult({
|
|
success: true,
|
|
message: "쿼리 실행 성공",
|
|
rowCount: result.rowCount || 0,
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
setTestResult({ success: false, message: error.message || "네트워크 오류" });
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 rounded-lg border p-4">
|
|
<h5 className="text-sm font-semibold">Database 설정</h5>
|
|
|
|
{/* 커넥션 타입 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">데이터베이스 연결</Label>
|
|
<RadioGroup
|
|
value={dataSource.connectionType || "current"}
|
|
onValueChange={(value: "current" | "external") =>
|
|
onChange({ connectionType: value })
|
|
}
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="current" id={`current-\${dataSource.id}`} />
|
|
<Label
|
|
htmlFor={`current-\${dataSource.id}`}
|
|
className="text-xs font-normal"
|
|
>
|
|
현재 데이터베이스
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="external" id={`external-\${dataSource.id}`} />
|
|
<Label
|
|
htmlFor={`external-\${dataSource.id}`}
|
|
className="text-xs font-normal"
|
|
>
|
|
외부 데이터베이스
|
|
</Label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
|
|
{/* 외부 DB 선택 */}
|
|
{dataSource.connectionType === "external" && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`external-conn-\${dataSource.id}`} className="text-xs">
|
|
외부 데이터베이스 선택 *
|
|
</Label>
|
|
{loadingConnections ? (
|
|
<div className="flex h-10 items-center justify-center rounded-md border">
|
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<Select
|
|
value={dataSource.externalConnectionId || ""}
|
|
onValueChange={(value) => onChange({ externalConnectionId: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="외부 DB 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{externalConnections.map((conn) => (
|
|
<SelectItem key={conn.id} value={conn.id} className="text-xs">
|
|
{conn.name} ({conn.type})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* SQL 쿼리 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`query-\${dataSource.id}`} className="text-xs">
|
|
SQL 쿼리 *
|
|
</Label>
|
|
<Textarea
|
|
id={`query-\${dataSource.id}`}
|
|
value={dataSource.query || ""}
|
|
onChange={(e) => onChange({ query: e.target.value })}
|
|
placeholder="SELECT * FROM table_name WHERE ..."
|
|
className="min-h-[120px] font-mono text-xs"
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
SELECT 쿼리만 허용됩니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 테스트 버튼 */}
|
|
<div className="space-y-2 border-t pt-4">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleTestQuery}
|
|
disabled={testing || !dataSource.query}
|
|
className="h-8 w-full gap-2 text-xs"
|
|
>
|
|
{testing ? (
|
|
<>
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
테스트 중...
|
|
</>
|
|
) : (
|
|
"쿼리 테스트"
|
|
)}
|
|
</Button>
|
|
|
|
{testResult && (
|
|
<div
|
|
className={`flex items-center gap-2 rounded-md p-2 text-xs \${
|
|
testResult.success
|
|
? "bg-green-50 text-green-700"
|
|
: "bg-red-50 text-red-700"
|
|
}`}
|
|
>
|
|
{testResult.success ? (
|
|
<CheckCircle className="h-3 w-3" />
|
|
) : (
|
|
<XCircle className="h-3 w-3" />
|
|
)}
|
|
<div>
|
|
{testResult.message}
|
|
{testResult.rowCount !== undefined && (
|
|
<span className="ml-1">({testResult.rowCount}행)</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|