제어관리 외부커넥션 설정기능
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ArrowLeft, ArrowRight, Table, Search, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// API import
|
||||
import { getTablesFromConnection, getBatchTablesWithColumns } from "@/lib/api/multiConnection";
|
||||
|
||||
// 타입 import
|
||||
import { Connection, TableInfo } from "@/lib/types/multiConnection";
|
||||
|
||||
interface TableStepProps {
|
||||
fromConnection?: Connection;
|
||||
toConnection?: Connection;
|
||||
fromTable?: TableInfo;
|
||||
toTable?: TableInfo;
|
||||
onSelectTable: (type: "from" | "to", table: TableInfo) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 📋 2단계: 테이블 선택
|
||||
* - FROM/TO 테이블 선택
|
||||
* - 테이블 검색 기능
|
||||
* - 컬럼 수 정보 표시
|
||||
*/
|
||||
const TableStep: React.FC<TableStepProps> = ({
|
||||
fromConnection,
|
||||
toConnection,
|
||||
fromTable,
|
||||
toTable,
|
||||
onSelectTable,
|
||||
onNext,
|
||||
onBack,
|
||||
}) => {
|
||||
const [fromTables, setFromTables] = useState<TableInfo[]>([]);
|
||||
const [toTables, setToTables] = useState<TableInfo[]>([]);
|
||||
const [fromSearch, setFromSearch] = useState("");
|
||||
const [toSearch, setToSearch] = useState("");
|
||||
const [isLoadingFrom, setIsLoadingFrom] = useState(false);
|
||||
const [isLoadingTo, setIsLoadingTo] = useState(false);
|
||||
const [tableColumnCounts, setTableColumnCounts] = useState<Record<string, number>>({});
|
||||
|
||||
// FROM 테이블 목록 로드 (배치 조회)
|
||||
useEffect(() => {
|
||||
if (fromConnection) {
|
||||
const loadFromTables = async () => {
|
||||
try {
|
||||
setIsLoadingFrom(true);
|
||||
console.log("🚀 FROM 테이블 배치 조회 시작");
|
||||
|
||||
// 배치 조회로 테이블 정보와 컬럼 수를 한번에 가져오기
|
||||
const batchResult = await getBatchTablesWithColumns(fromConnection.id);
|
||||
|
||||
console.log("✅ FROM 테이블 배치 조회 완료:", batchResult);
|
||||
|
||||
// TableInfo 형식으로 변환
|
||||
const tables: TableInfo[] = batchResult.map((item) => ({
|
||||
tableName: item.tableName,
|
||||
displayName: item.displayName || item.tableName,
|
||||
}));
|
||||
|
||||
setFromTables(tables);
|
||||
|
||||
// 컬럼 수 정보를 state에 저장
|
||||
const columnCounts: Record<string, number> = {};
|
||||
batchResult.forEach((item) => {
|
||||
columnCounts[`from_${item.tableName}`] = item.columnCount;
|
||||
});
|
||||
|
||||
setTableColumnCounts((prev) => ({
|
||||
...prev,
|
||||
...columnCounts,
|
||||
}));
|
||||
|
||||
console.log(`📊 FROM 테이블 ${tables.length}개 로드 완료, 컬럼 수:`, columnCounts);
|
||||
} catch (error) {
|
||||
console.error("FROM 테이블 목록 로드 실패:", error);
|
||||
toast.error("소스 테이블 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setIsLoadingFrom(false);
|
||||
}
|
||||
};
|
||||
loadFromTables();
|
||||
}
|
||||
}, [fromConnection]);
|
||||
|
||||
// TO 테이블 목록 로드 (배치 조회)
|
||||
useEffect(() => {
|
||||
if (toConnection) {
|
||||
const loadToTables = async () => {
|
||||
try {
|
||||
setIsLoadingTo(true);
|
||||
console.log("🚀 TO 테이블 배치 조회 시작");
|
||||
|
||||
// 배치 조회로 테이블 정보와 컬럼 수를 한번에 가져오기
|
||||
const batchResult = await getBatchTablesWithColumns(toConnection.id);
|
||||
|
||||
console.log("✅ TO 테이블 배치 조회 완료:", batchResult);
|
||||
|
||||
// TableInfo 형식으로 변환
|
||||
const tables: TableInfo[] = batchResult.map((item) => ({
|
||||
tableName: item.tableName,
|
||||
displayName: item.displayName || item.tableName,
|
||||
}));
|
||||
|
||||
setToTables(tables);
|
||||
|
||||
// 컬럼 수 정보를 state에 저장
|
||||
const columnCounts: Record<string, number> = {};
|
||||
batchResult.forEach((item) => {
|
||||
columnCounts[`to_${item.tableName}`] = item.columnCount;
|
||||
});
|
||||
|
||||
setTableColumnCounts((prev) => ({
|
||||
...prev,
|
||||
...columnCounts,
|
||||
}));
|
||||
|
||||
console.log(`📊 TO 테이블 ${tables.length}개 로드 완료, 컬럼 수:`, columnCounts);
|
||||
} catch (error) {
|
||||
console.error("TO 테이블 목록 로드 실패:", error);
|
||||
toast.error("대상 테이블 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setIsLoadingTo(false);
|
||||
}
|
||||
};
|
||||
loadToTables();
|
||||
}
|
||||
}, [toConnection]);
|
||||
|
||||
// 테이블 필터링
|
||||
const filteredFromTables = fromTables.filter((table) =>
|
||||
(table.displayName || table.tableName).toLowerCase().includes(fromSearch.toLowerCase()),
|
||||
);
|
||||
|
||||
const filteredToTables = toTables.filter((table) =>
|
||||
(table.displayName || table.tableName).toLowerCase().includes(toSearch.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleTableSelect = (type: "from" | "to", tableName: string) => {
|
||||
const tables = type === "from" ? fromTables : toTables;
|
||||
const table = tables.find((t) => t.tableName === tableName);
|
||||
if (table) {
|
||||
onSelectTable(type, table);
|
||||
}
|
||||
};
|
||||
|
||||
const canProceed = fromTable && toTable;
|
||||
|
||||
const renderTableItem = (table: TableInfo, type: "from" | "to") => {
|
||||
const displayName =
|
||||
table.displayName && table.displayName !== table.tableName ? table.displayName : table.tableName;
|
||||
|
||||
const columnCount = tableColumnCounts[`${type}_${table.tableName}`];
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Table className="h-4 w-4" />
|
||||
<span>{displayName}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{columnCount !== undefined ? columnCount : table.columnCount || 0}개 컬럼
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Table className="h-5 w-5" />
|
||||
2단계: 테이블 선택
|
||||
</CardTitle>
|
||||
<p className="text-muted-foreground text-sm">연결된 데이터베이스에서 소스와 대상 테이블을 선택하세요.</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="max-h-[calc(100vh-400px)] min-h-[400px] space-y-6 overflow-y-auto">
|
||||
{/* FROM 테이블 선택 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">FROM 테이블 (소스)</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{fromConnection?.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
|
||||
<Input
|
||||
placeholder="테이블 검색..."
|
||||
value={fromSearch}
|
||||
onChange={(e) => setFromSearch(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
{isLoadingFrom ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<span className="text-sm">테이블 목록 로드 중...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={fromTable?.tableName || ""} onValueChange={(value) => handleTableSelect("from", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="소스 테이블을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filteredFromTables.map((table) => (
|
||||
<SelectItem key={table.tableName} value={table.tableName}>
|
||||
{renderTableItem(table, "from")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{fromTable && (
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">{fromTable.displayName || fromTable.tableName}</span>
|
||||
<Badge variant="secondary">
|
||||
📊 {tableColumnCounts[`from_${fromTable.tableName}`] || fromTable.columnCount || 0}개 컬럼
|
||||
</Badge>
|
||||
</div>
|
||||
{fromTable.description && <p className="text-muted-foreground text-xs">{fromTable.description}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TO 테이블 선택 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">TO 테이블 (대상)</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{toConnection?.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
|
||||
<Input
|
||||
placeholder="테이블 검색..."
|
||||
value={toSearch}
|
||||
onChange={(e) => setToSearch(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
{isLoadingTo ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<span className="text-sm">테이블 목록 로드 중...</span>
|
||||
</div>
|
||||
) : (
|
||||
<Select value={toTable?.tableName || ""} onValueChange={(value) => handleTableSelect("to", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="대상 테이블을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filteredToTables.map((table) => (
|
||||
<SelectItem key={table.tableName} value={table.tableName}>
|
||||
{renderTableItem(table, "to")}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{toTable && (
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">{toTable.displayName || toTable.tableName}</span>
|
||||
<Badge variant="secondary">
|
||||
📊 {tableColumnCounts[`to_${toTable.tableName}`] || toTable.columnCount || 0}개 컬럼
|
||||
</Badge>
|
||||
</div>
|
||||
{toTable.description && <p className="text-muted-foreground text-xs">{toTable.description}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 테이블 매핑 표시 */}
|
||||
{fromTable && toTable && (
|
||||
<div className="bg-primary/5 border-primary/20 rounded-lg border p-4">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<div className="text-center">
|
||||
<div className="font-medium">{fromTable.displayName || fromTable.tableName}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{tableColumnCounts[`from_${fromTable.tableName}`] || fromTable.columnCount || 0}개 컬럼
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArrowRight className="text-primary h-5 w-5" />
|
||||
|
||||
<div className="text-center">
|
||||
<div className="font-medium">{toTable.displayName || toTable.tableName}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{tableColumnCounts[`to_${toTable.tableName}`] || toTable.columnCount || 0}개 컬럼
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<Badge variant="outline" className="text-primary">
|
||||
💡 테이블 매핑: {fromTable.displayName || fromTable.tableName} →{" "}
|
||||
{toTable.displayName || toTable.tableName}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 네비게이션 버튼 */}
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button variant="outline" onClick={onBack} className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
이전: 연결 선택
|
||||
</Button>
|
||||
|
||||
<Button onClick={onNext} disabled={!canProceed} className="flex items-center gap-2">
|
||||
다음: 컬럼 매핑
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableStep;
|
||||
Reference in New Issue
Block a user