344 lines
13 KiB
TypeScript
344 lines
13 KiB
TypeScript
"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;
|