관계 수정 , 삭제 구현
This commit is contained in:
parent
fdd849fa0d
commit
db509bb3d9
|
|
@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
||||||
import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow";
|
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo } from "@/lib/api/dataflow";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
// 연결 정보 타입
|
// 연결 정보 타입
|
||||||
|
|
@ -112,25 +112,57 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
bodyTemplate: "{}",
|
bodyTemplate: "{}",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 테이블 및 컬럼 선택을 위한 새로운 상태들
|
||||||
|
const [availableTables, setAvailableTables] = useState<TableInfo[]>([]);
|
||||||
|
const [selectedFromTable, setSelectedFromTable] = useState<string>("");
|
||||||
|
const [selectedToTable, setSelectedToTable] = useState<string>("");
|
||||||
|
const [fromTableColumns, setFromTableColumns] = useState<ColumnInfo[]>([]);
|
||||||
|
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
|
||||||
|
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
||||||
|
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 테이블 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTables = async () => {
|
||||||
|
try {
|
||||||
|
const tables = await DataFlowAPI.getTables();
|
||||||
|
setAvailableTables(tables);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 목록 로드 실패:", error);
|
||||||
|
toast.error("테이블 목록을 불러오는데 실패했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
loadTables();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
// 모달이 열릴 때 기본값 설정
|
// 모달이 열릴 때 기본값 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && connection) {
|
if (isOpen && connection) {
|
||||||
const fromTableName = connection.fromNode.displayName;
|
const fromTableName = connection.fromNode.tableName;
|
||||||
const toTableName = connection.toNode.displayName;
|
const toTableName = connection.toNode.tableName;
|
||||||
|
const fromDisplayName = connection.fromNode.displayName;
|
||||||
|
const toDisplayName = connection.toNode.displayName;
|
||||||
|
|
||||||
|
// 테이블 선택 설정
|
||||||
|
setSelectedFromTable(fromTableName);
|
||||||
|
setSelectedToTable(toTableName);
|
||||||
|
|
||||||
setConfig({
|
setConfig({
|
||||||
relationshipName: `${fromTableName} → ${toTableName}`,
|
relationshipName: `${fromDisplayName} → ${toDisplayName}`,
|
||||||
relationshipType: "one-to-one",
|
relationshipType: "one-to-one",
|
||||||
connectionType: "simple-key",
|
connectionType: "simple-key",
|
||||||
fromColumnName: "",
|
fromColumnName: "",
|
||||||
toColumnName: "",
|
toColumnName: "",
|
||||||
description: `${fromTableName}과 ${toTableName} 간의 데이터 관계`,
|
description: `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
||||||
settings: {},
|
settings: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 단순 키값 연결 기본값 설정
|
// 단순 키값 연결 기본값 설정
|
||||||
setSimpleKeySettings({
|
setSimpleKeySettings({
|
||||||
notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`,
|
notes: `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 데이터 저장 기본값 설정
|
// 데이터 저장 기본값 설정
|
||||||
|
|
@ -148,9 +180,67 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
headers: "{}",
|
headers: "{}",
|
||||||
bodyTemplate: "{}",
|
bodyTemplate: "{}",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 선택된 컬럼 정보가 있다면 설정
|
||||||
|
if (connection.selectedColumnsData) {
|
||||||
|
const fromColumns = connection.selectedColumnsData[fromTableName]?.columns || [];
|
||||||
|
const toColumns = connection.selectedColumnsData[toTableName]?.columns || [];
|
||||||
|
|
||||||
|
setSelectedFromColumns(fromColumns);
|
||||||
|
setSelectedToColumns(toColumns);
|
||||||
|
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
fromColumnName: fromColumns.join(", "),
|
||||||
|
toColumnName: toColumns.join(", "),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen, connection]);
|
}, [isOpen, connection]);
|
||||||
|
|
||||||
|
// From 테이블 선택 시 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFromColumns = async () => {
|
||||||
|
if (selectedFromTable) {
|
||||||
|
try {
|
||||||
|
const columns = await DataFlowAPI.getTableColumns(selectedFromTable);
|
||||||
|
setFromTableColumns(columns);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("From 테이블 컬럼 로드 실패:", error);
|
||||||
|
toast.error("From 테이블 컬럼을 불러오는데 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFromColumns();
|
||||||
|
}, [selectedFromTable]);
|
||||||
|
|
||||||
|
// To 테이블 선택 시 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadToColumns = async () => {
|
||||||
|
if (selectedToTable) {
|
||||||
|
try {
|
||||||
|
const columns = await DataFlowAPI.getTableColumns(selectedToTable);
|
||||||
|
setToTableColumns(columns);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("To 테이블 컬럼 로드 실패:", error);
|
||||||
|
toast.error("To 테이블 컬럼을 불러오는데 실패했습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadToColumns();
|
||||||
|
}, [selectedToTable]);
|
||||||
|
|
||||||
|
// 선택된 컬럼들이 변경될 때 config 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
fromColumnName: selectedFromColumns.join(", "),
|
||||||
|
toColumnName: selectedToColumns.join(", "),
|
||||||
|
}));
|
||||||
|
}, [selectedFromColumns, selectedToColumns]);
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (!config.relationshipName || !connection) {
|
if (!config.relationshipName || !connection) {
|
||||||
toast.error("필수 정보를 모두 입력해주세요.");
|
toast.error("필수 정보를 모두 입력해주세요.");
|
||||||
|
|
@ -172,27 +262,23 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택된 컬럼들 추출
|
// 선택된 컬럼들 검증
|
||||||
const selectedColumnsData = connection.selectedColumnsData || {};
|
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
|
||||||
const tableNames = Object.keys(selectedColumnsData);
|
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
|
||||||
const fromTable = tableNames[0];
|
|
||||||
const toTable = tableNames[1];
|
|
||||||
|
|
||||||
const fromColumns = selectedColumnsData[fromTable]?.columns || [];
|
|
||||||
const toColumns = selectedColumnsData[toTable]?.columns || [];
|
|
||||||
|
|
||||||
if (fromColumns.length === 0 || toColumns.length === 0) {
|
|
||||||
toast.error("선택된 컬럼이 없습니다.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 선택된 테이블과 컬럼 정보 사용
|
||||||
|
const fromTableName = selectedFromTable || connection.fromNode.tableName;
|
||||||
|
const toTableName = selectedToTable || connection.toNode.tableName;
|
||||||
|
|
||||||
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
||||||
const relationshipData: TableRelationship = {
|
const relationshipData: TableRelationship = {
|
||||||
relationship_name: config.relationshipName,
|
relationship_name: config.relationshipName,
|
||||||
from_table_name: connection.fromNode.tableName,
|
from_table_name: fromTableName,
|
||||||
to_table_name: connection.toNode.tableName,
|
to_table_name: toTableName,
|
||||||
from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
from_column_name: selectedFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||||
to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분
|
to_column_name: selectedToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
||||||
relationship_type: config.relationshipType as any,
|
relationship_type: config.relationshipType as any,
|
||||||
connection_type: config.connectionType as any,
|
connection_type: config.connectionType as any,
|
||||||
company_code: companyCode,
|
company_code: companyCode,
|
||||||
|
|
@ -200,15 +286,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
...settings,
|
...settings,
|
||||||
description: config.description,
|
description: config.description,
|
||||||
multiColumnMapping: {
|
multiColumnMapping: {
|
||||||
fromColumns: fromColumns,
|
fromColumns: selectedFromColumns,
|
||||||
toColumns: toColumns,
|
toColumns: selectedToColumns,
|
||||||
fromTable: selectedColumnsData[fromTable]?.displayName || fromTable,
|
fromTable: fromTableName,
|
||||||
toTable: selectedColumnsData[toTable]?.displayName || toTable,
|
toTable: toTableName,
|
||||||
},
|
},
|
||||||
isMultiColumn: fromColumns.length > 1 || toColumns.length > 1,
|
isMultiColumn: selectedFromColumns.length > 1 || selectedToColumns.length > 1,
|
||||||
columnCount: {
|
columnCount: {
|
||||||
from: fromColumns.length,
|
from: selectedFromColumns.length,
|
||||||
to: toColumns.length,
|
to: selectedToColumns.length,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -445,28 +531,128 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 연결 정보 표시 */}
|
{/* 테이블 및 컬럼 선택 */}
|
||||||
<div className="rounded-lg border bg-gray-50 p-3">
|
<div className="rounded-lg border bg-gray-50 p-4">
|
||||||
<div className="mb-2 text-sm font-medium">연결 정보</div>
|
<div className="mb-4 text-sm font-medium">테이블 및 컬럼 선택</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<span className="font-medium">{fromTableData?.displayName || fromTable}</span>
|
{/* 테이블 선택 */}
|
||||||
<span className="text-xs text-gray-500">({fromTable})</span>
|
<div className="mb-4 grid grid-cols-2 gap-4">
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400" />
|
<div>
|
||||||
<span className="font-medium">{toTableData?.displayName || toTable}</span>
|
<Label className="text-xs font-medium text-gray-600">From 테이블</Label>
|
||||||
<span className="text-xs text-gray-500">({toTable})</span>
|
<Select value={selectedFromTable} onValueChange={setSelectedFromTable}>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="테이블 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableTables.map((table) => (
|
||||||
|
<SelectItem key={table.tableName} value={table.tableName}>
|
||||||
|
{table.displayName} ({table.tableName})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium text-gray-600">To 테이블</Label>
|
||||||
|
<Select value={selectedToTable} onValueChange={setSelectedToTable}>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="테이블 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{availableTables.map((table) => (
|
||||||
|
<SelectItem key={table.tableName} value={table.tableName}>
|
||||||
|
{table.displayName} ({table.tableName})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
|
||||||
{fromTableData?.columns.map((column, index) => (
|
{/* 컬럼 선택 */}
|
||||||
<Badge key={`${fromTable}-${column}-${index}`} variant="outline" className="text-xs">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{column}
|
<div>
|
||||||
</Badge>
|
<Label className="text-xs font-medium text-gray-600">From 컬럼</Label>
|
||||||
))}
|
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
||||||
{toTableData?.columns.map((column, index) => (
|
{fromTableColumns.map((column) => (
|
||||||
<Badge key={`${toTable}-${column}-${index}`} variant="secondary" className="text-xs">
|
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
||||||
{column}
|
<input
|
||||||
</Badge>
|
type="checkbox"
|
||||||
))}
|
checked={selectedFromColumns.includes(column.columnName)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedFromColumns((prev) => [...prev, column.columnName]);
|
||||||
|
} else {
|
||||||
|
setSelectedFromColumns((prev) => prev.filter((col) => col !== column.columnName));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span>{column.columnName}</span>
|
||||||
|
<span className="text-xs text-gray-500">({column.dataType})</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
{fromTableColumns.length === 0 && (
|
||||||
|
<div className="py-2 text-xs text-gray-500">
|
||||||
|
{selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium text-gray-600">To 컬럼</Label>
|
||||||
|
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
||||||
|
{toTableColumns.map((column) => (
|
||||||
|
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedToColumns.includes(column.columnName)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedToColumns((prev) => [...prev, column.columnName]);
|
||||||
|
} else {
|
||||||
|
setSelectedToColumns((prev) => prev.filter((col) => col !== column.columnName));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span>{column.columnName}</span>
|
||||||
|
<span className="text-xs text-gray-500">({column.dataType})</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
{toTableColumns.length === 0 && (
|
||||||
|
<div className="py-2 text-xs text-gray-500">
|
||||||
|
{selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 선택된 컬럼 미리보기 */}
|
||||||
|
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
|
||||||
|
<div className="mt-4 flex items-center gap-2 text-sm">
|
||||||
|
<span className="font-medium">{selectedFromTable}</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{selectedFromColumns.map((column) => (
|
||||||
|
<Badge key={column} variant="outline" className="text-xs">
|
||||||
|
{column}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="h-4 w-4 text-gray-400" />
|
||||||
|
<span className="font-medium">{selectedToTable}</span>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{selectedToColumns.map((column) => (
|
||||||
|
<Badge key={column} variant="secondary" className="text-xs">
|
||||||
|
{column}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 기본 연결 설정 */}
|
{/* 기본 연결 설정 */}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
|
const [showSaveModal, setShowSaveModal] = useState(false); // 저장 모달 표시 상태
|
||||||
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
|
const [isSaving, setIsSaving] = useState(false); // 저장 중 상태
|
||||||
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
|
const [currentDiagramName, setCurrentDiagramName] = useState<string>(""); // 현재 편집 중인 관계도 이름
|
||||||
|
const [selectedEdgeForEdit, setSelectedEdgeForEdit] = useState<Edge | null>(null); // 수정/삭제할 엣지
|
||||||
|
const [showEdgeActions, setShowEdgeActions] = useState(false); // 엣지 액션 버튼 표시 상태
|
||||||
|
const [edgeActionPosition, setEdgeActionPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); // 액션 버튼 위치
|
||||||
|
const [editingRelationshipId, setEditingRelationshipId] = useState<string | null>(null); // 현재 수정 중인 관계 ID
|
||||||
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
const toastShownRef = useRef(false); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
|
||||||
// 편집 모드일 때 관계도 이름 로드
|
// 편집 모드일 때 관계도 이름 로드
|
||||||
|
|
@ -436,7 +440,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
if (selectedEdgeInfo) {
|
if (selectedEdgeInfo) {
|
||||||
setSelectedEdgeInfo(null);
|
setSelectedEdgeInfo(null);
|
||||||
}
|
}
|
||||||
}, [selectedEdgeInfo]);
|
if (showEdgeActions) {
|
||||||
|
setShowEdgeActions(false);
|
||||||
|
setSelectedEdgeForEdit(null);
|
||||||
|
}
|
||||||
|
// 컬럼 선택 해제
|
||||||
|
setSelectedColumns({});
|
||||||
|
}, [selectedEdgeInfo, showEdgeActions]);
|
||||||
|
|
||||||
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
// 빈 onConnect 함수 (드래그 연결 비활성화)
|
||||||
const onConnect = useCallback(() => {
|
const onConnect = useCallback(() => {
|
||||||
|
|
@ -444,7 +454,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
return;
|
return;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 엣지 클릭 시 연결 정보 표시
|
// 엣지 클릭 시 연결 정보 표시 및 관련 컬럼 하이라이트
|
||||||
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const edgeData = edge.data as {
|
const edgeData = edge.data as {
|
||||||
|
|
@ -462,7 +472,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
connectionType: string;
|
connectionType: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (edgeData) {
|
if (edgeData) {
|
||||||
|
// 엣지 정보 설정
|
||||||
setSelectedEdgeInfo({
|
setSelectedEdgeInfo({
|
||||||
relationshipId: edgeData.relationshipId,
|
relationshipId: edgeData.relationshipId,
|
||||||
relationshipName: edgeData.relationshipName || "관계",
|
relationshipName: edgeData.relationshipName || "관계",
|
||||||
|
|
@ -474,6 +486,26 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
connectionType: edgeData.connectionType,
|
connectionType: edgeData.connectionType,
|
||||||
connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`,
|
connectionInfo: edgeData.details?.connectionInfo || `${edgeData.fromTable} → ${edgeData.toTable}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 관련 컬럼 하이라이트
|
||||||
|
const newSelectedColumns: { [tableName: string]: string[] } = {};
|
||||||
|
|
||||||
|
// fromTable의 컬럼들 선택
|
||||||
|
if (edgeData.fromTable && edgeData.fromColumns) {
|
||||||
|
newSelectedColumns[edgeData.fromTable] = [...edgeData.fromColumns];
|
||||||
|
}
|
||||||
|
|
||||||
|
// toTable의 컬럼들 선택
|
||||||
|
if (edgeData.toTable && edgeData.toColumns) {
|
||||||
|
newSelectedColumns[edgeData.toTable] = [...edgeData.toColumns];
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedColumns(newSelectedColumns);
|
||||||
|
|
||||||
|
// 액션 버튼 표시
|
||||||
|
setSelectedEdgeForEdit(edge);
|
||||||
|
setEdgeActionPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
setShowEdgeActions(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -637,15 +669,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
[handleColumnClick, selectedColumns, setNodes],
|
[handleColumnClick, selectedColumns, setNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 노드 전체 삭제
|
// 기존 clearNodes 함수 제거 (중복 방지)
|
||||||
const clearNodes = useCallback(() => {
|
|
||||||
setNodes([]);
|
|
||||||
setEdges([]);
|
|
||||||
setSelectedColumns({});
|
|
||||||
setSelectionOrder([]);
|
|
||||||
setSelectedNodes([]);
|
|
||||||
setCurrentDiagramId(null); // 현재 diagram_id도 초기화
|
|
||||||
}, [setNodes, setEdges]);
|
|
||||||
|
|
||||||
// 현재 추가된 테이블명 목록 가져오기
|
// 현재 추가된 테이블명 목록 가져오기
|
||||||
const getSelectedTableNames = useCallback(() => {
|
const getSelectedTableNames = useCallback(() => {
|
||||||
|
|
@ -671,7 +695,7 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
|
|
||||||
// JSON 형태의 관계 객체 생성
|
// JSON 형태의 관계 객체 생성
|
||||||
const newRelationship: JsonRelationship = {
|
const newRelationship: JsonRelationship = {
|
||||||
id: generateUniqueId("rel", Date.now()),
|
id: editingRelationshipId || generateUniqueId("rel", Date.now()), // 수정 모드면 기존 ID 사용
|
||||||
fromTable,
|
fromTable,
|
||||||
toTable,
|
toTable,
|
||||||
fromColumns,
|
fromColumns,
|
||||||
|
|
@ -681,6 +705,13 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
settings: relationship.settings || {},
|
settings: relationship.settings || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 수정 모드인 경우 기존 관계를 교체
|
||||||
|
if (editingRelationshipId) {
|
||||||
|
setTempRelationships((prev) => prev.filter((rel) => rel.id !== editingRelationshipId));
|
||||||
|
setEdges((prev) => prev.filter((edge) => edge.data?.relationshipId !== editingRelationshipId));
|
||||||
|
setEditingRelationshipId(null); // 수정 모드 해제
|
||||||
|
}
|
||||||
|
|
||||||
// 메모리에 관계 추가
|
// 메모리에 관계 추가
|
||||||
setTempRelationships((prev) => [...prev, newRelationship]);
|
setTempRelationships((prev) => [...prev, newRelationship]);
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
|
|
@ -730,7 +761,11 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
// 연결 설정 취소
|
// 연결 설정 취소
|
||||||
const handleCancelConnection = useCallback(() => {
|
const handleCancelConnection = useCallback(() => {
|
||||||
setPendingConnection(null);
|
setPendingConnection(null);
|
||||||
}, []);
|
// 수정 모드였다면 해제
|
||||||
|
if (editingRelationshipId) {
|
||||||
|
setEditingRelationshipId(null);
|
||||||
|
}
|
||||||
|
}, [editingRelationshipId]);
|
||||||
|
|
||||||
// 관계도 저장 함수
|
// 관계도 저장 함수
|
||||||
const handleSaveDiagram = useCallback(
|
const handleSaveDiagram = useCallback(
|
||||||
|
|
@ -796,12 +831,9 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
|
|
||||||
// 저장 모달 열기
|
// 저장 모달 열기
|
||||||
const handleOpenSaveModal = useCallback(() => {
|
const handleOpenSaveModal = useCallback(() => {
|
||||||
if (tempRelationships.length === 0) {
|
// 관계가 0개여도 저장 가능하도록 수정
|
||||||
toast.error("저장할 관계가 없습니다. 먼저 테이블을 연결해주세요.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowSaveModal(true);
|
setShowSaveModal(true);
|
||||||
}, [tempRelationships.length]);
|
}, []);
|
||||||
|
|
||||||
// 저장 모달 닫기
|
// 저장 모달 닫기
|
||||||
const handleCloseSaveModal = useCallback(() => {
|
const handleCloseSaveModal = useCallback(() => {
|
||||||
|
|
@ -810,6 +842,158 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
}
|
}
|
||||||
}, [isSaving]);
|
}, [isSaving]);
|
||||||
|
|
||||||
|
// 고립된 노드 제거 함수
|
||||||
|
const removeOrphanedNodes = useCallback(
|
||||||
|
(updatedRelationships: JsonRelationship[], showMessage = true) => {
|
||||||
|
setNodes((currentNodes) => {
|
||||||
|
// 현재 관계에서 사용되는 테이블들 추출
|
||||||
|
const usedTables = new Set<string>();
|
||||||
|
updatedRelationships.forEach((rel) => {
|
||||||
|
usedTables.add(rel.fromTable);
|
||||||
|
usedTables.add(rel.toTable);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 사용되지 않는 노드들 찾기
|
||||||
|
const orphanedNodes = currentNodes.filter((node) => {
|
||||||
|
const tableName = node.data.table.tableName;
|
||||||
|
return !usedTables.has(tableName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 연결된 노드들만 유지
|
||||||
|
const connectedNodes = currentNodes.filter((node) => {
|
||||||
|
const tableName = node.data.table.tableName;
|
||||||
|
return usedTables.has(tableName);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orphanedNodes.length > 0 && showMessage) {
|
||||||
|
const orphanedTableNames = orphanedNodes.map((node) => node.data.table.displayName).join(", ");
|
||||||
|
toast(`${orphanedNodes.length}개의 연결되지 않은 테이블 노드가 제거되었습니다: ${orphanedTableNames}`, {
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectedNodes;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setNodes],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 엣지 삭제 핸들러
|
||||||
|
const handleDeleteEdge = useCallback(() => {
|
||||||
|
if (!selectedEdgeForEdit) return;
|
||||||
|
|
||||||
|
const edgeData = selectedEdgeForEdit.data as {
|
||||||
|
relationshipId: string;
|
||||||
|
fromTable: string;
|
||||||
|
toTable: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// tempRelationships에서 해당 관계 제거
|
||||||
|
const updatedRelationships = tempRelationships.filter((rel) => rel.id !== edgeData.relationshipId);
|
||||||
|
setTempRelationships(updatedRelationships);
|
||||||
|
|
||||||
|
// 엣지 제거
|
||||||
|
setEdges((prev) => prev.filter((edge) => edge.id !== selectedEdgeForEdit.id));
|
||||||
|
|
||||||
|
// 고립된 노드 제거
|
||||||
|
removeOrphanedNodes(updatedRelationships);
|
||||||
|
|
||||||
|
// 상태 초기화
|
||||||
|
setShowEdgeActions(false);
|
||||||
|
setSelectedEdgeForEdit(null);
|
||||||
|
setSelectedEdgeInfo(null);
|
||||||
|
setSelectedColumns({});
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
|
||||||
|
toast.success("관계가 삭제되었습니다.");
|
||||||
|
}, [selectedEdgeForEdit, tempRelationships, setEdges, removeOrphanedNodes]);
|
||||||
|
|
||||||
|
// 엣지 수정 핸들러 (수정 모드 전환)
|
||||||
|
const handleEditEdge = useCallback(() => {
|
||||||
|
if (!selectedEdgeForEdit) return;
|
||||||
|
|
||||||
|
const edgeData = selectedEdgeForEdit.data as {
|
||||||
|
relationshipId: string;
|
||||||
|
relationshipName: string;
|
||||||
|
fromTable: string;
|
||||||
|
toTable: string;
|
||||||
|
fromColumns: string[];
|
||||||
|
toColumns: string[];
|
||||||
|
relationshipType: string;
|
||||||
|
connectionType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기존 관계 찾기
|
||||||
|
const existingRelationship = tempRelationships.find((rel) => rel.id === edgeData.relationshipId);
|
||||||
|
if (!existingRelationship) {
|
||||||
|
toast.error("수정할 관계를 찾을 수 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수정 모드로 전환 (관계는 제거하지 않음)
|
||||||
|
setEditingRelationshipId(edgeData.relationshipId);
|
||||||
|
|
||||||
|
// 기존 관계를 기반으로 연결 정보 구성
|
||||||
|
const fromNode = nodes.find((node) => node.data.table.tableName === edgeData.fromTable);
|
||||||
|
const toNode = nodes.find((node) => node.data.table.tableName === edgeData.toTable);
|
||||||
|
|
||||||
|
if (!fromNode || !toNode) {
|
||||||
|
toast.error("연결된 테이블을 찾을 수 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionInfo = {
|
||||||
|
fromNode: {
|
||||||
|
id: fromNode.id,
|
||||||
|
tableName: fromNode.data.table.tableName,
|
||||||
|
displayName: fromNode.data.table.displayName,
|
||||||
|
},
|
||||||
|
toNode: {
|
||||||
|
id: toNode.id,
|
||||||
|
tableName: toNode.data.table.tableName,
|
||||||
|
displayName: toNode.data.table.displayName,
|
||||||
|
},
|
||||||
|
selectedColumnsData: {
|
||||||
|
[edgeData.fromTable]: {
|
||||||
|
displayName: fromNode.data.table.displayName,
|
||||||
|
columns: edgeData.fromColumns,
|
||||||
|
},
|
||||||
|
[edgeData.toTable]: {
|
||||||
|
displayName: toNode.data.table.displayName,
|
||||||
|
columns: edgeData.toColumns,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ConnectionSetupModal을 위한 연결 정보 설정
|
||||||
|
setPendingConnection(connectionInfo);
|
||||||
|
|
||||||
|
// 상태 초기화
|
||||||
|
setShowEdgeActions(false);
|
||||||
|
setSelectedEdgeForEdit(null);
|
||||||
|
setSelectedEdgeInfo(null);
|
||||||
|
|
||||||
|
toast("관계 수정 모드입니다. 원하는 대로 설정을 변경하고 확인을 눌러주세요.", {
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}, [selectedEdgeForEdit, tempRelationships, nodes]);
|
||||||
|
|
||||||
|
// 전체 삭제 핸들러
|
||||||
|
const clearNodes = useCallback(() => {
|
||||||
|
setNodes([]);
|
||||||
|
setEdges([]);
|
||||||
|
setTempRelationships([]);
|
||||||
|
setSelectedColumns({});
|
||||||
|
setSelectedNodes([]);
|
||||||
|
setPendingConnection(null);
|
||||||
|
setSelectedEdgeInfo(null);
|
||||||
|
setShowEdgeActions(false);
|
||||||
|
setSelectedEdgeForEdit(null);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
|
||||||
|
toast.success("모든 테이블과 관계가 삭제되었습니다.");
|
||||||
|
}, [setNodes, setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="data-flow-designer h-screen bg-gray-100">
|
<div className="data-flow-designer h-screen bg-gray-100">
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
|
|
@ -827,19 +1011,26 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
|
|
||||||
{/* 컨트롤 버튼들 */}
|
{/* 컨트롤 버튼들 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => removeOrphanedNodes(tempRelationships)}
|
||||||
|
className="w-full rounded-lg bg-orange-500 p-3 font-medium text-white transition-colors hover:bg-orange-600"
|
||||||
|
disabled={nodes.length === 0}
|
||||||
|
>
|
||||||
|
🧹 고립된 노드 정리
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={clearNodes}
|
onClick={clearNodes}
|
||||||
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
className="w-full rounded-lg bg-red-500 p-3 font-medium text-white transition-colors hover:bg-red-600"
|
||||||
>
|
>
|
||||||
전체 삭제
|
🗑️ 전체 삭제
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenSaveModal}
|
onClick={handleOpenSaveModal}
|
||||||
disabled={tempRelationships.length === 0}
|
className={`w-full rounded-lg bg-green-500 p-3 font-medium text-white transition-colors hover:bg-green-600 ${
|
||||||
className={`w-full rounded-lg p-3 font-medium text-white transition-colors ${
|
hasUnsavedChanges ? "animate-pulse" : ""
|
||||||
tempRelationships.length > 0 ? "bg-green-500 hover:bg-green-600" : "cursor-not-allowed bg-gray-400"
|
}`}
|
||||||
} ${hasUnsavedChanges ? "animate-pulse" : ""}`}
|
|
||||||
>
|
>
|
||||||
💾 관계도 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
💾 관계도 저장 {tempRelationships.length > 0 && `(${tempRelationships.length})`}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1033,6 +1224,30 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
||||||
onCancel={handleCancelConnection}
|
onCancel={handleCancelConnection}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 엣지 액션 버튼 */}
|
||||||
|
{showEdgeActions && selectedEdgeForEdit && (
|
||||||
|
<div
|
||||||
|
className="fixed z-50 flex gap-2 rounded-lg border bg-white p-2 shadow-lg"
|
||||||
|
style={{
|
||||||
|
left: edgeActionPosition.x - 80,
|
||||||
|
top: edgeActionPosition.y - 60,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleEditEdge}
|
||||||
|
className="flex items-center gap-1 rounded bg-blue-500 px-3 py-1 text-xs text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
✏️ 수정
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteEdge}
|
||||||
|
className="flex items-center gap-1 rounded bg-red-500 px-3 py-1 text-xs text-white hover:bg-red-600"
|
||||||
|
>
|
||||||
|
🗑️ 삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 관계도 저장 모달 */}
|
{/* 관계도 저장 모달 */}
|
||||||
<SaveDiagramModal
|
<SaveDiagramModal
|
||||||
isOpen={showSaveModal}
|
isOpen={showSaveModal}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue