200 lines
6.6 KiB
TypeScript
200 lines
6.6 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 { ArrowLeft, Link, Loader2, CheckCircle } from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
// API import
|
||
|
|
import { getColumnsFromConnection } from "@/lib/api/multiConnection";
|
||
|
|
|
||
|
|
// 타입 import
|
||
|
|
import { Connection, TableInfo, ColumnInfo } from "@/lib/types/multiConnection";
|
||
|
|
import { FieldMapping } from "../types/redesigned";
|
||
|
|
|
||
|
|
// 컴포넌트 import
|
||
|
|
import FieldMappingCanvas from "./VisualMapping/FieldMappingCanvas";
|
||
|
|
|
||
|
|
interface FieldMappingStepProps {
|
||
|
|
fromTable?: TableInfo;
|
||
|
|
toTable?: TableInfo;
|
||
|
|
fromConnection?: Connection;
|
||
|
|
toConnection?: Connection;
|
||
|
|
fieldMappings: FieldMapping[];
|
||
|
|
onCreateMapping: (fromField: ColumnInfo, toField: ColumnInfo) => void;
|
||
|
|
onDeleteMapping: (mappingId: string) => void;
|
||
|
|
onNext: () => void;
|
||
|
|
onBack: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 🎯 3단계: 시각적 필드 매핑
|
||
|
|
* - SVG 기반 연결선 표시
|
||
|
|
* - 드래그 앤 드롭 지원 (향후)
|
||
|
|
* - 실시간 매핑 업데이트
|
||
|
|
*/
|
||
|
|
const FieldMappingStep: React.FC<FieldMappingStepProps> = ({
|
||
|
|
fromTable,
|
||
|
|
toTable,
|
||
|
|
fromConnection,
|
||
|
|
toConnection,
|
||
|
|
fieldMappings,
|
||
|
|
onCreateMapping,
|
||
|
|
onDeleteMapping,
|
||
|
|
onNext,
|
||
|
|
onBack,
|
||
|
|
}) => {
|
||
|
|
const [fromColumns, setFromColumns] = useState<ColumnInfo[]>([]);
|
||
|
|
const [toColumns, setToColumns] = useState<ColumnInfo[]>([]);
|
||
|
|
const [isLoading, setIsLoading] = useState(false);
|
||
|
|
|
||
|
|
// 컬럼 정보 로드
|
||
|
|
useEffect(() => {
|
||
|
|
const loadColumns = async () => {
|
||
|
|
console.log("🔍 컬럼 로딩 시작:", {
|
||
|
|
fromConnection: fromConnection?.id,
|
||
|
|
toConnection: toConnection?.id,
|
||
|
|
fromTable: fromTable?.tableName,
|
||
|
|
toTable: toTable?.tableName,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!fromConnection || !toConnection || !fromTable || !toTable) {
|
||
|
|
console.warn("⚠️ 필수 정보 누락:", {
|
||
|
|
fromConnection: !!fromConnection,
|
||
|
|
toConnection: !!toConnection,
|
||
|
|
fromTable: !!fromTable,
|
||
|
|
toTable: !!toTable,
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
setIsLoading(true);
|
||
|
|
console.log("📡 API 호출 시작:", {
|
||
|
|
fromAPI: `getColumnsFromConnection(${fromConnection.id}, "${fromTable.tableName}")`,
|
||
|
|
toAPI: `getColumnsFromConnection(${toConnection.id}, "${toTable.tableName}")`,
|
||
|
|
});
|
||
|
|
|
||
|
|
const [fromCols, toCols] = await Promise.all([
|
||
|
|
getColumnsFromConnection(fromConnection.id, fromTable.tableName),
|
||
|
|
getColumnsFromConnection(toConnection.id, toTable.tableName),
|
||
|
|
]);
|
||
|
|
|
||
|
|
console.log("🔍 원본 API 응답 확인:", {
|
||
|
|
fromCols: fromCols,
|
||
|
|
toCols: toCols,
|
||
|
|
fromType: typeof fromCols,
|
||
|
|
toType: typeof toCols,
|
||
|
|
fromIsArray: Array.isArray(fromCols),
|
||
|
|
toIsArray: Array.isArray(toCols),
|
||
|
|
});
|
||
|
|
|
||
|
|
// 안전한 배열 처리
|
||
|
|
const safeFromCols = Array.isArray(fromCols) ? fromCols : [];
|
||
|
|
const safeToCols = Array.isArray(toCols) ? toCols : [];
|
||
|
|
|
||
|
|
console.log("✅ 컬럼 로딩 성공:", {
|
||
|
|
fromColumns: safeFromCols.length,
|
||
|
|
toColumns: safeToCols.length,
|
||
|
|
fromData: safeFromCols.slice(0, 2), // 처음 2개만 로깅
|
||
|
|
toData: safeToCols.slice(0, 2),
|
||
|
|
originalFromType: typeof fromCols,
|
||
|
|
originalToType: typeof toCols,
|
||
|
|
});
|
||
|
|
|
||
|
|
setFromColumns(safeFromCols);
|
||
|
|
setToColumns(safeToCols);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ 컬럼 정보 로드 실패:", error);
|
||
|
|
toast.error("필드 정보를 불러오는데 실패했습니다.");
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
loadColumns();
|
||
|
|
}, [fromConnection, toConnection, fromTable, toTable]);
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<CardContent className="flex items-center justify-center py-12">
|
||
|
|
<Loader2 className="mr-2 h-6 w-6 animate-spin" />
|
||
|
|
<span>필드 정보를 불러오는 중...</span>
|
||
|
|
</CardContent>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<CardHeader className="pb-2">
|
||
|
|
<CardTitle className="flex items-center gap-2">
|
||
|
|
<Link className="h-5 w-5" />
|
||
|
|
3단계: 컬럼 매핑
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
|
||
|
|
<CardContent className="flex h-full flex-col p-0">
|
||
|
|
{/* 매핑 캔버스 - 전체 영역 사용 */}
|
||
|
|
<div className="min-h-0 flex-1 p-4">
|
||
|
|
{isLoading ? (
|
||
|
|
<div className="flex h-full items-center justify-center">
|
||
|
|
<div className="text-muted-foreground">컬럼 정보를 불러오는 중...</div>
|
||
|
|
</div>
|
||
|
|
) : fromColumns.length > 0 && toColumns.length > 0 ? (
|
||
|
|
<FieldMappingCanvas
|
||
|
|
fromFields={fromColumns}
|
||
|
|
toFields={toColumns}
|
||
|
|
mappings={fieldMappings}
|
||
|
|
onCreateMapping={onCreateMapping}
|
||
|
|
onDeleteMapping={onDeleteMapping}
|
||
|
|
/>
|
||
|
|
) : (
|
||
|
|
<div className="flex h-full flex-col items-center justify-center space-y-3">
|
||
|
|
<div className="text-muted-foreground">컬럼 정보를 찾을 수 없습니다.</div>
|
||
|
|
<div className="text-muted-foreground text-xs">
|
||
|
|
FROM 컬럼: {fromColumns.length}개, TO 컬럼: {toColumns.length}개
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => {
|
||
|
|
console.log("🔄 수동 재로딩 시도");
|
||
|
|
setFromColumns([]);
|
||
|
|
setToColumns([]);
|
||
|
|
// useEffect가 재실행되도록 강제 업데이트
|
||
|
|
setIsLoading(true);
|
||
|
|
setTimeout(() => setIsLoading(false), 100);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
다시 시도
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 하단 네비게이션 - 고정 */}
|
||
|
|
<div className="flex-shrink-0 border-t bg-white p-4">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<Button variant="outline" onClick={onBack} className="flex items-center gap-2">
|
||
|
|
<ArrowLeft className="h-4 w-4" />
|
||
|
|
이전
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
<div className="text-muted-foreground text-sm">
|
||
|
|
{fieldMappings.length > 0 ? `${fieldMappings.length}개 매핑 완료` : "컬럼을 선택해서 매핑하세요"}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Button onClick={onNext} disabled={fieldMappings.length === 0} className="flex items-center gap-2">
|
||
|
|
<CheckCircle className="h-4 w-4" />
|
||
|
|
저장
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default FieldMappingStep;
|