테이블 연결 생성방식 수정

This commit is contained in:
hyeonsu
2025-09-15 15:12:02 +09:00
parent 353d8d2bb0
commit e459025d8a
6 changed files with 481 additions and 429 deletions

View File

@@ -70,6 +70,11 @@ interface DataFlowDesignerProps {
// TableRelationship 타입은 dataflow.ts에서 import
// 내부에서 사용할 확장된 JsonRelationship 타입 (connectionType 포함)
interface ExtendedJsonRelationship extends JsonRelationship {
connectionType: string;
}
export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
companyCode: propCompanyCode = "*",
diagramId,
@@ -88,7 +93,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
const [selectedColumns, setSelectedColumns] = useState<{
[tableName: string]: string[];
}>({});
const [selectionOrder, setSelectionOrder] = useState<string[]>([]);
// selectionOrder는 더 이상 사용하지 않음 (테이블 노드 선택 방식으로 변경)
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [pendingConnection, setPendingConnection] = useState<{
fromNode: { id: string; tableName: string; displayName: string };
@@ -103,7 +108,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
};
existingRelationship?: {
relationshipName: string;
relationshipType: string;
connectionType: string;
settings?: any;
};
@@ -117,17 +121,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toTable: string;
fromColumns: string[];
toColumns: string[];
relationshipType: string;
connectionType: string;
connectionInfo: string;
} | null>(null); // 선택된 엣지 정보
// 새로운 메모리 기반 상태들
const [tempRelationships, setTempRelationships] = useState<JsonRelationship[]>([]); // 메모리에 저장된 관계들
const [tempRelationships, setTempRelationships] = useState<ExtendedJsonRelationship[]>([]); // 메모리에 저장된 관계들
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 저장되지 않은 변경사항
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
const [currentDiagramCategory, setCurrentDiagramCategory] = useState<string>("simple-key"); // 현재 관계도의 연결 종류
const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState<Edge | null>(null); // 수정/삭제할 엣지
const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태
const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치
@@ -174,8 +178,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
return newColumns;
});
// 선택 순서도 정리
setSelectionOrder((prev) => prev.filter((tableName) => !deletedTableNames.includes(tableName)));
// selectionOrder는 더 이상 사용하지 않음
// 선택된 노드 초기화
setSelectedNodes([]);
@@ -188,49 +191,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedNodes, setNodes]);
// 컬럼 클릭 처리 (토글 방식, 최대 2개 테이블만 허용)
// 컬럼 클릭 처리 비활성화 (테이블 노드 선택 방식으로 변경)
const handleColumnClick = useCallback((tableName: string, columnName: string) => {
setSelectedColumns((prev) => {
const currentColumns = prev[tableName] || [];
const isSelected = currentColumns.includes(columnName);
const selectedTables = Object.keys(prev).filter((name) => prev[name] && prev[name].length > 0);
if (isSelected) {
// 선택 해제
const newColumns = currentColumns.filter((column) => column !== columnName);
if (newColumns.length === 0) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [tableName]: removed, ...rest } = prev;
// 선택 순서에서도 제거 (다음 렌더링에서)
setTimeout(() => {
setSelectionOrder((order) => order.filter((name) => name !== tableName));
}, 0);
return rest;
}
return { ...prev, [tableName]: newColumns };
} else {
// 새 선택
if (selectedTables.length >= 2 && !selectedTables.includes(tableName)) {
toast.error("최대 2개 테이블까지만 선택할 수 있습니다.");
return prev;
}
const newColumns = [...currentColumns, columnName];
const newSelection = { ...prev, [tableName]: newColumns };
// 선택 순서 업데이트 (다음 렌더링에서)
setTimeout(() => {
setSelectionOrder((order) => {
if (!order.includes(tableName)) {
return [...order, tableName];
}
return order;
});
}, 0);
return newSelection;
}
});
// 컬럼 클릭으로는 더 이상 선택하지 않음
console.log(`컬럼 클릭 무시됨: ${tableName}.${columnName}`);
return;
}, []);
// 선택된 관계도의 관계 로드
@@ -257,14 +222,17 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
console.log("📊 테이블 목록:", tableNames);
// 기존 데이터에서 relationshipName이 없는 경우 기본값 설정
const normalizedRelationships = relationships.map((rel: JsonRelationship) => ({
// category를 각 관계의 connectionType으로 복원
const normalizedRelationships: ExtendedJsonRelationship[] = relationships.map((rel: JsonRelationship) => ({
...rel,
relationshipName: rel.relationshipName || `${rel.fromTable}${rel.toTable}`, // 기본값 설정
connectionType: jsonDiagram.category || "simple-key", // 관계도의 category를 각 관계의 connectionType으로 복원
}));
// 메모리에 관계 저장 (기존 관계도 편집 시)
setTempRelationships(normalizedRelationships);
setCurrentDiagramId(currentDiagramId);
setCurrentDiagramCategory(jsonDiagram.category || "simple-key"); // 관계도의 연결 종류 설정
// 테이블 노드 생성을 위한 테이블 정보 로드
@@ -368,7 +336,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// JSON 관계를 엣지로 변환하여 표시 (테이블 간 번들 연결)
const relationshipEdges: Edge[] = [];
relationships.forEach((rel: JsonRelationship) => {
normalizedRelationships.forEach((rel: ExtendedJsonRelationship) => {
const fromTable = rel.fromTable;
const toTable = rel.toTable;
const fromColumns = rel.fromColumns || [];
@@ -394,7 +362,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
data: {
relationshipId: rel.id,
relationshipName: rel.relationshipName,
relationshipType: rel.relationshipType,
connectionType: rel.connectionType,
fromTable: fromTable,
toTable: toTable,
@@ -403,7 +370,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 클릭 시 표시할 상세 정보
details: {
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
relationshipType: rel.relationshipType,
connectionType: rel.connectionType,
},
},
@@ -448,10 +414,56 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
}
}, [companyCode, diagramId, relationshipId, loadExistingRelationships, loadSelectedDiagramRelationships]);
// 노드 선택 변경 핸들러
const onSelectionChange = useCallback(({ nodes }: { nodes: Node<TableNodeData>[] }) => {
const selectedNodeIds = nodes.map((node) => node.id);
setSelectedNodes(selectedNodeIds);
// 노드 클릭 핸들러 (커스텀 다중 선택 구현)
const onNodeClick = useCallback(
(event: React.MouseEvent, node: Node<TableNodeData>) => {
event.stopPropagation();
const nodeId = node.id;
const isCurrentlySelected = selectedNodes.includes(nodeId);
if (isCurrentlySelected) {
// 이미 선택된 노드를 클릭하면 선택 해제
const newSelection = selectedNodes.filter((id) => id !== nodeId);
setSelectedNodes(newSelection);
// React Flow 노드 상태 업데이트
setNodes((prevNodes) =>
prevNodes.map((n) => ({
...n,
selected: newSelection.includes(n.id),
})),
);
} else {
// 새로운 노드 선택
let newSelection: string[];
if (selectedNodes.length >= 2) {
// 이미 2개가 선택되어 있으면 첫 번째를 제거하고 새로운 것을 추가 (FIFO)
newSelection = [selectedNodes[1], nodeId];
} else {
// 2개 미만이면 추가
newSelection = [...selectedNodes, nodeId];
}
setSelectedNodes(newSelection);
// React Flow 노드 상태 업데이트
setNodes((prevNodes) =>
prevNodes.map((n) => ({
...n,
selected: newSelection.includes(n.id),
})),
);
}
},
[selectedNodes, setNodes],
);
// 노드 선택 변경 핸들러 (React Flow 자체 선택 이벤트는 무시)
const onSelectionChange = useCallback(() => {
// React Flow의 자동 선택 변경은 무시하고 우리의 커스텀 로직만 사용
// 이 함수는 비워두거나 최소한의 동기화만 수행
}, []);
// 캔버스 클릭 시 엣지 정보 섹션 닫기
@@ -483,11 +495,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toTable: string;
fromColumns: string[];
toColumns: string[];
relationshipType: string;
connectionType: string;
details?: {
connectionInfo: string;
relationshipType: string;
connectionType: string;
};
};
@@ -501,7 +511,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toTable: edgeData.toTable,
fromColumns: edgeData.fromColumns || [],
toColumns: edgeData.toColumns || [],
relationshipType: edgeData.relationshipType,
connectionType: edgeData.connectionType,
connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable}${edgeData.toTable}`,
});
@@ -582,44 +591,25 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
})),
);
// selectionOrder에서 선택되지 않은 테이블들 제거
const activeTables = Object.keys(selectedColumns).filter(
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
);
setSelectionOrder((prev) => prev.filter((tableName) => activeTables.includes(tableName)));
// selectionOrder는 더 이상 사용하지 않음
}, [selectedColumns, setNodes]);
// 연결 가능한 상태인지 확인
// 연결 가능한 상태인지 확인 (테이블 노드 선택 기반으로 변경)
const canCreateConnection = () => {
const selectedTables = Object.keys(selectedColumns).filter(
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
);
// 최소 2개의 서로 다른 테이블에서 컬럼이 선택되어야 함
return selectedTables.length >= 2;
// 최소 2개의 테이블 노드가 선택되어야 함
return selectedNodes.length >= 2;
};
// 컬럼 연결 설정 모달 열기
// 테이블 노드 연결 설정 모달 열기 (컬럼 선택 불필요)
const openConnectionModal = () => {
const selectedTables = Object.keys(selectedColumns).filter(
(tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0,
);
if (selectedNodes.length < 2) return;
if (selectedTables.length < 2) return;
// 선택 순서에 따라 첫 번째와 두 번째 테이블 설정
const orderedTables = selectionOrder.filter((name) => selectedTables.includes(name));
const firstTableName = orderedTables[0];
const secondTableName = orderedTables[1];
const firstNode = nodes.find((node) => node.data.table.tableName === firstTableName);
const secondNode = nodes.find((node) => node.data.table.tableName === secondTableName);
// 선택된 첫 번째와 두 번째 노드 찾기
const firstNode = nodes.find((node) => node.id === selectedNodes[0]);
const secondNode = nodes.find((node) => node.id === selectedNodes[1]);
if (!firstNode || !secondNode) return;
// 첫 번째로 선택된 컬럼들 가져오기
const firstTableColumns = selectedColumns[firstTableName] || [];
const secondTableColumns = selectedColumns[secondTableName] || [];
setPendingConnection({
fromNode: {
id: firstNode.id,
@@ -631,24 +621,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
tableName: secondNode.data.table.tableName,
displayName: secondNode.data.table.displayName,
},
// 선택된 첫 번째 컬럼을 연결 컬럼으로 설정
fromColumn: firstTableColumns[0] || "",
toColumn: secondTableColumns[0] || "",
// 선택된 모든 컬럼 정보를 선택 순서대로 전달
selectedColumnsData: (() => {
const orderedData: { [key: string]: { displayName: string; columns: string[] } } = {};
// selectionOrder 순서대로 데이터 구성 (첫 번째 선택이 먼저)
orderedTables.forEach((tableName) => {
const node = nodes.find((n) => n.data.table.tableName === tableName);
if (node && selectedColumns[tableName]) {
orderedData[tableName] = {
displayName: node.data.table.displayName,
columns: selectedColumns[tableName],
};
}
});
return orderedData;
})(),
// 컬럼 선택 정보는 제거 (단순 키값 연결에서만 필요)
});
};
@@ -712,15 +685,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
.map((col) => col.trim())
.filter((col) => col);
// JSON 형태의 관계 객체 생성
const newRelationship: JsonRelationship = {
// JSON 형태의 관계 객체 생성 (중복 필드 제거)
const newRelationship: ExtendedJsonRelationship = {
id: editingRelationshipId || generateUniqueId("rel", Date.now()), // 수정 모드면 기존 ID 사용
relationshipName: relationship.relationship_name, // 연결 이름 추가
fromTable,
toTable,
fromColumns,
toColumns,
relationshipType: relationship.relationship_type,
connectionType: relationship.connection_type,
settings: relationship.settings || {},
};
@@ -736,6 +708,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setTempRelationships((prev) => [...prev, newRelationship]);
setHasUnsavedChanges(true);
// 첫 번째 관계가 추가되면 관계도의 category를 해당 connectionType으로 설정
if (tempRelationships.length === 0) {
setCurrentDiagramCategory(relationship.connection_type);
}
// 캔버스에 엣지 즉시 표시
const newEdge: Edge = {
id: generateUniqueId("edge", Date.now()),
@@ -751,7 +728,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
data: {
relationshipId: newRelationship.id,
relationshipName: newRelationship.relationshipName,
relationshipType: relationship.relationship_type,
connectionType: relationship.connection_type,
fromTable,
toTable,
@@ -759,7 +735,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toColumns,
details: {
connectionInfo: `${fromTable}(${fromColumns.join(", ")}) → ${toTable}(${toColumns.join(", ")})`,
relationshipType: relationship.relationship_type,
connectionType: relationship.connection_type,
},
},
@@ -770,7 +745,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 관계 생성 후 선택된 컬럼들 초기화
setSelectedColumns({});
setSelectionOrder([]);
console.log("메모리에 관계 생성 완료:", newRelationship);
toast.success("관계가 생성되었습니다. 저장 버튼을 눌러 관계도를 저장하세요.");
@@ -792,17 +766,19 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 관계도 저장 함수
const handleSaveDiagram = useCallback(
async (diagramName: string) => {
if (tempRelationships.length === 0) {
toast.error("저장할 관계가 없습니다.");
// 🔥 수정: 관계가 없어도 노드가 있으면 저장 가능
if (nodes.length === 0) {
toast.error("저장할 테이블이 없습니다.");
return;
}
setIsSaving(true);
try {
// 연결된 테이블 목록 추출
const connectedTables = Array.from(
new Set([...tempRelationships.map((rel) => rel.fromTable), ...tempRelationships.map((rel) => rel.toTable)]),
).sort();
// 🔥 수정: 현재 캔버스의 모든 테이블 기반으로 변경
const connectedTables = nodes
.map((node) => node.data?.table?.tableName)
.filter((tableName) => tableName)
.sort();
// 현재 노드 위치 추출
const nodePositions: NodePositions = {};
@@ -815,16 +791,33 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
}
});
console.log("🔍 저장할 노드 위치 정보:", nodePositions);
console.log("📊 현재 노드 개수:", nodes.length);
console.log("📋 연결된 테이블 목록:", connectedTables);
console.log("🔗 관계 개수:", tempRelationships.length);
// 관계도의 주요 연결 타입 결정 (첫 번째 관계의 connectionType 사용)
const primaryConnectionType = tempRelationships.length > 0 ? tempRelationships[0].connectionType : "simple-key";
// connectionType을 관계에서 제거하고 관계도 레벨로 이동
const relationshipsWithoutConnectionType = tempRelationships.map((rel) => {
const { connectionType, ...relationshipWithoutType } = rel;
return relationshipWithoutType;
});
// 저장 요청 데이터 생성
const createRequest: CreateDiagramRequest = {
diagram_name: diagramName,
relationships: {
relationships: tempRelationships,
relationships: relationshipsWithoutConnectionType,
tables: connectedTables,
},
node_positions: nodePositions,
category: primaryConnectionType, // connectionType을 관계도 레벨의 category로 이동
};
console.log("🚀 API 요청 데이터:", JSON.stringify(createRequest, null, 2));
let savedDiagram;
// 편집 모드 vs 신규 생성 모드 구분
@@ -860,7 +853,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setIsSaving(false);
}
},
[tempRelationships, diagramId, companyCode, user?.userId],
[tempRelationships, diagramId, companyCode, user?.userId, nodes],
);
// 저장 모달 열기
@@ -878,7 +871,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 고립된 노드 제거 함수
const removeOrphanedNodes = useCallback(
(updatedRelationships: JsonRelationship[], showMessage = true) => {
(updatedRelationships: ExtendedJsonRelationship[], showMessage = true) => {
setNodes((currentNodes) => {
// 현재 관계에서 사용되는 테이블들 추출
const usedTables = new Set<string>();
@@ -953,7 +946,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
toTable: string;
fromColumns: string[];
toColumns: string[];
relationshipType: string;
connectionType: string;
};
@@ -1000,7 +992,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 기존 관계 정보 추가 (연결 이름 유지를 위해)
existingRelationship: {
relationshipName: existingRelationship.relationshipName,
relationshipType: existingRelationship.relationshipType,
connectionType: existingRelationship.connectionType,
settings: existingRelationship.settings,
},
@@ -1097,6 +1088,14 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<span> ID:</span>
<span className="font-medium">{currentDiagramId || "미설정"}</span>
</div>
<div className="flex justify-between">
<span> :</span>
<span className="font-medium">
{currentDiagramCategory === "simple-key" && "단순 키값"}
{currentDiagramCategory === "data-save" && "데이터 저장"}
{currentDiagramCategory === "external-call" && "외부 호출"}
</span>
</div>
{hasUnsavedChanges && (
<div className="mt-2 text-xs font-medium text-orange-600"> </div>
)}
@@ -1117,6 +1116,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onSelectionChange={onSelectionChange}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onEdgeClick={onEdgeClick}
onEdgeMouseEnter={onEdgeMouseEnter}
@@ -1129,7 +1129,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
zoomOnScroll={true}
zoomOnPinch={true}
panOnDrag={[1, 2]}
selectionOnDrag={true}
selectionOnDrag={false}
multiSelectionKeyCode={null}
selectNodesOnDrag={false}
selectionMode={SelectionMode.Partial}
@@ -1138,100 +1138,116 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
<Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#E5E7EB" />
</ReactFlow>
{/* 선택된 컬럼 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
{Object.keys(selectedColumns).length > 0 &&
!showEdgeActions &&
!pendingConnection &&
!editingRelationshipId && (
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
<span className="text-sm text-blue-600">📋</span>
</div>
<div>
<div className="text-sm font-semibold text-gray-800"> </div>
<div className="text-xs text-gray-500">{Object.keys(selectedColumns).length} </div>
{/* 선택된 테이블 노드 팝업 - 캔버스 좌측 상단 고정 (새 관계 생성 시에만 표시) */}
{selectedNodes.length > 0 && !showEdgeActions && !pendingConnection && !editingRelationshipId && (
<div className="pointer-events-auto absolute top-4 left-4 z-40 w-80 rounded-xl border border-blue-200 bg-white shadow-lg">
{/* 헤더 */}
<div className="flex items-center justify-between rounded-t-xl border-b border-blue-100 bg-gradient-to-r from-blue-50 to-indigo-50 p-3">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100">
<span className="text-sm text-blue-600">📋</span>
</div>
<div>
<div className="text-sm font-semibold text-gray-800"> </div>
<div className="text-xs text-gray-500">
{selectedNodes.length === 1
? "FROM 테이블 선택됨"
: selectedNodes.length === 2
? "FROM → TO 연결 준비"
: `${selectedNodes.length}개 테이블`}
</div>
</div>
<button
onClick={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
>
</button>
</div>
<button
onClick={() => {
setSelectedNodes([]);
}}
className="flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
>
</button>
</div>
{/* 컨텐츠 */}
<div className="max-h-80 overflow-y-auto p-3">
<div className="space-y-3">
{[...new Set(selectionOrder)]
.filter((tableName) => selectedColumns[tableName] && selectedColumns[tableName].length > 0)
.map((tableName, index, filteredOrder) => {
const columns = selectedColumns[tableName];
const node = nodes.find((n) => n.data.table.tableName === tableName);
const displayName = node?.data.table.displayName || tableName;
return (
<div key={`selected-${tableName}-${index}`}>
{/* 테이블 정보 */}
<div className="rounded-lg bg-blue-50 p-2">
<div className="mb-1 text-xs font-medium text-blue-700">{displayName}</div>
<div className="flex flex-wrap gap-1">
{columns.map((column, columnIndex) => (
<span
key={`${tableName}-${column}-${columnIndex}`}
className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
title={column}
>
{column}
</span>
))}
</div>
{/* 컨텐츠 */}
<div className="max-h-80 overflow-y-auto p-3">
<div className="space-y-3">
{selectedNodes.map((nodeId, index) => {
const node = nodes.find((n) => n.id === nodeId);
if (!node) return null;
const { tableName, displayName } = node.data.table;
return (
<div key={`selected-${nodeId}-${index}`}>
{/* 테이블 정보 */}
<div
className={`rounded-lg p-2 ${
index === 0
? "border-l-4 border-emerald-400 bg-emerald-50"
: index === 1
? "border-l-4 border-blue-400 bg-blue-50"
: "bg-gray-50"
}`}
>
<div className="mb-1 flex items-center justify-between">
<div
className={`text-xs font-medium ${
index === 0 ? "text-emerald-700" : index === 1 ? "text-blue-700" : "text-gray-700"
}`}
>
{displayName}
</div>
{/* 화살표 */}
{index === 0 && filteredOrder.length > 1 && (
<div className="flex justify-center py-1">
<div className="text-sm text-gray-400"></div>
{selectedNodes.length === 2 && (
<div
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-blue-800"
}`}
>
{index === 0 ? "FROM" : "TO"}
</div>
)}
</div>
);
})}
</div>
</div>
<div className="text-xs text-gray-600">{tableName}</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-blue-100 p-3">
<button
onClick={openConnectionModal}
disabled={!canCreateConnection()}
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
canCreateConnection()
? "bg-blue-500 text-white hover:bg-blue-600"
: "cursor-not-allowed bg-gray-300 text-gray-500"
}`}
>
<span>🔗</span>
<span> </span>
</button>
<button
onClick={() => {
setSelectedColumns({});
setSelectionOrder([]);
}}
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
>
<span>🗑</span>
<span></span>
</button>
{/* 연결 화살표 (마지막이 아닌 경우) */}
{index < selectedNodes.length - 1 && (
<div className="flex justify-center py-1">
<div className="text-gray-400"></div>
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* 액션 버튼 */}
<div className="flex gap-2 border-t border-blue-100 p-3">
<button
onClick={openConnectionModal}
disabled={!canCreateConnection()}
className={`flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs font-medium transition-colors ${
canCreateConnection()
? "bg-blue-500 text-white hover:bg-blue-600"
: "cursor-not-allowed bg-gray-300 text-gray-500"
}`}
>
<span>🔗</span>
<span> </span>
</button>
<button
onClick={() => {
setSelectedColumns({});
setSelectedNodes([]);
}}
className="flex flex-1 items-center justify-center gap-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-300"
>
<span>🗑</span>
<span></span>
</button>
</div>
</div>
)}
{/* 안내 메시지 */}
{nodes.length === 0 && (
@@ -1296,13 +1312,6 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
{/* 관계 정보 요약 */}
<div className="border-b border-gray-100 bg-gray-50 p-3">
<div className="flex items-center justify-center gap-4">
<div className="text-center">
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase"> </div>
<div className="mt-1 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-800">
{selectedEdgeInfo.relationshipType}
</div>
</div>
<div className="text-gray-300">|</div>
<div className="text-center">
<div className="text-xs font-medium tracking-wide text-gray-500 uppercase"> </div>
<div className="mt-1 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-800">