"use client"; /** * DELETE 액션 노드 속성 편집 */ import { useEffect, useState, useCallback } from "react"; import { Plus, Trash2, AlertTriangle, Database, Globe, Link2, Check, ChevronsUpDown } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; import { tableTypeApi } from "@/lib/api/screen"; import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections"; import type { DeleteActionNodeData } from "@/types/node-editor"; import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections"; interface DeleteActionPropertiesProps { nodeId: string; data: DeleteActionNodeData; } // 소스 필드 타입 interface SourceField { name: string; label?: string; } const OPERATORS = [ { value: "EQUALS", label: "=" }, { value: "NOT_EQUALS", label: "≠" }, { value: "GREATER_THAN", label: ">" }, { value: "LESS_THAN", label: "<" }, { value: "IN", label: "IN" }, { value: "NOT_IN", label: "NOT IN" }, ] as const; export function DeleteActionProperties({ nodeId, data }: DeleteActionPropertiesProps) { const { updateNode, getExternalConnectionsCache, nodes, edges } = useFlowEditorStore(); // 🔥 타겟 타입 상태 const [targetType, setTargetType] = useState<"internal" | "external" | "api">(data.targetType || "internal"); const [displayName, setDisplayName] = useState(data.displayName || `${data.targetTable} 삭제`); const [targetTable, setTargetTable] = useState(data.targetTable); const [whereConditions, setWhereConditions] = useState(data.whereConditions || []); // 🆕 소스 필드 목록 (연결된 입력 노드에서 가져오기) const [sourceFields, setSourceFields] = useState([]); const [sourceFieldsOpenState, setSourceFieldsOpenState] = useState([]); // 🔥 외부 DB 관련 상태 const [externalConnections, setExternalConnections] = useState([]); const [externalConnectionsLoading, setExternalConnectionsLoading] = useState(false); const [selectedExternalConnectionId, setSelectedExternalConnectionId] = useState( data.externalConnectionId, ); const [externalTables, setExternalTables] = useState([]); const [externalTablesLoading, setExternalTablesLoading] = useState(false); const [externalTargetTable, setExternalTargetTable] = useState(data.externalTargetTable); const [externalColumns, setExternalColumns] = useState([]); const [externalColumnsLoading, setExternalColumnsLoading] = useState(false); // 🔥 REST API 관련 상태 (DELETE는 요청 바디 없음) const [apiEndpoint, setApiEndpoint] = useState(data.apiEndpoint || ""); const [apiAuthType, setApiAuthType] = useState<"none" | "basic" | "bearer" | "apikey">(data.apiAuthType || "none"); const [apiAuthConfig, setApiAuthConfig] = useState(data.apiAuthConfig || {}); const [apiHeaders, setApiHeaders] = useState>(data.apiHeaders || {}); // 🔥 내부 DB 테이블 관련 상태 const [tables, setTables] = useState([]); const [tablesLoading, setTablesLoading] = useState(false); const [tablesOpen, setTablesOpen] = useState(false); const [selectedTableLabel, setSelectedTableLabel] = useState(data.targetTable); // 내부 DB 컬럼 관련 상태 interface ColumnInfo { columnName: string; columnLabel?: string; dataType: string; isNullable: boolean; } const [targetColumns, setTargetColumns] = useState([]); const [columnsLoading, setColumnsLoading] = useState(false); // Combobox 열림 상태 관리 const [fieldOpenState, setFieldOpenState] = useState([]); useEffect(() => { setDisplayName(data.displayName || `${data.targetTable} 삭제`); setTargetTable(data.targetTable); setWhereConditions(data.whereConditions || []); }, [data]); // 🔥 내부 DB 테이블 목록 로딩 useEffect(() => { if (targetType === "internal") { loadTables(); } }, [targetType]); // 🔥 외부 커넥션 로딩 useEffect(() => { if (targetType === "external") { loadExternalConnections(); } }, [targetType]); // 🔥 외부 테이블 로딩 useEffect(() => { if (targetType === "external" && selectedExternalConnectionId) { loadExternalTables(selectedExternalConnectionId); } }, [targetType, selectedExternalConnectionId]); // 🔥 외부 컬럼 로딩 useEffect(() => { if (targetType === "external" && selectedExternalConnectionId && externalTargetTable) { loadExternalColumns(selectedExternalConnectionId, externalTargetTable); } }, [targetType, selectedExternalConnectionId, externalTargetTable]); // 🔥 내부 DB 컬럼 로딩 useEffect(() => { if (targetType === "internal" && targetTable) { loadColumns(targetTable); } }, [targetType, targetTable]); // whereConditions 변경 시 fieldOpenState 초기화 useEffect(() => { setFieldOpenState(new Array(whereConditions.length).fill(false)); setSourceFieldsOpenState(new Array(whereConditions.length).fill(false)); }, [whereConditions.length]); // 🆕 소스 필드 로딩 (연결된 입력 노드에서) const loadSourceFields = useCallback(async () => { // 현재 노드로 연결된 엣지 찾기 const incomingEdges = edges.filter((e) => e.target === nodeId); console.log("🔍 DELETE 노드 연결 엣지:", incomingEdges); if (incomingEdges.length === 0) { console.log("⚠️ 연결된 소스 노드가 없습니다"); setSourceFields([]); return; } const fields: SourceField[] = []; const processedFields = new Set(); for (const edge of incomingEdges) { const sourceNode = nodes.find((n) => n.id === edge.source); if (!sourceNode) continue; console.log("🔗 소스 노드:", sourceNode.type, sourceNode.data); // 소스 노드 타입에 따라 필드 추출 if (sourceNode.type === "trigger" && sourceNode.data.tableName) { // 트리거 노드: 테이블 컬럼 조회 try { const columns = await tableTypeApi.getColumns(sourceNode.data.tableName); if (columns && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; if (!processedFields.has(colName)) { processedFields.add(colName); fields.push({ name: colName, label: col.columnLabel || col.column_label || colName, }); } }); } } catch (error) { console.error("트리거 노드 컬럼 로딩 실패:", error); } } else if (sourceNode.type === "tableSource" && sourceNode.data.tableName) { // 테이블 소스 노드 try { const columns = await tableTypeApi.getColumns(sourceNode.data.tableName); if (columns && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; if (!processedFields.has(colName)) { processedFields.add(colName); fields.push({ name: colName, label: col.columnLabel || col.column_label || colName, }); } }); } } catch (error) { console.error("테이블 소스 노드 컬럼 로딩 실패:", error); } } else if (sourceNode.type === "condition") { // 조건 노드: 연결된 이전 노드에서 필드 가져오기 const conditionIncomingEdges = edges.filter((e) => e.target === sourceNode.id); for (const condEdge of conditionIncomingEdges) { const condSourceNode = nodes.find((n) => n.id === condEdge.source); if (condSourceNode?.type === "trigger" && condSourceNode.data.tableName) { try { const columns = await tableTypeApi.getColumns(condSourceNode.data.tableName); if (columns && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; if (!processedFields.has(colName)) { processedFields.add(colName); fields.push({ name: colName, label: col.columnLabel || col.column_label || colName, }); } }); } } catch (error) { console.error("조건 노드 소스 컬럼 로딩 실패:", error); } } } } } console.log("✅ DELETE 노드 소스 필드:", fields); setSourceFields(fields); }, [nodeId, nodes, edges]); // 소스 필드 로딩 useEffect(() => { loadSourceFields(); }, [loadSourceFields]); const loadExternalConnections = async () => { try { setExternalConnectionsLoading(true); const cached = getExternalConnectionsCache(); if (cached) { setExternalConnections(cached); return; } const data = await getTestedExternalConnections(); setExternalConnections(data); } catch (error) { console.error("외부 커넥션 로딩 실패:", error); } finally { setExternalConnectionsLoading(false); } }; const loadExternalTables = async (connectionId: number) => { try { setExternalTablesLoading(true); const data = await getExternalTables(connectionId); setExternalTables(data); } catch (error) { console.error("외부 테이블 로딩 실패:", error); } finally { setExternalTablesLoading(false); } }; const loadExternalColumns = async (connectionId: number, tableName: string) => { try { setExternalColumnsLoading(true); const data = await getExternalColumns(connectionId, tableName); setExternalColumns(data); } catch (error) { console.error("외부 컬럼 로딩 실패:", error); } finally { setExternalColumnsLoading(false); } }; const handleTargetTypeChange = (newType: "internal" | "external" | "api") => { setTargetType(newType); updateNode(nodeId, { targetType: newType, targetTable: newType === "internal" ? targetTable : undefined, externalConnectionId: newType === "external" ? selectedExternalConnectionId : undefined, externalTargetTable: newType === "external" ? externalTargetTable : undefined, apiEndpoint: newType === "api" ? apiEndpoint : undefined, apiAuthType: newType === "api" ? apiAuthType : undefined, apiAuthConfig: newType === "api" ? apiAuthConfig : undefined, apiHeaders: newType === "api" ? apiHeaders : undefined, }); }; // 🔥 테이블 목록 로딩 const loadTables = async () => { try { setTablesLoading(true); const tableList = await tableTypeApi.getTables(); setTables(tableList); } catch (error) { console.error("테이블 목록 로딩 실패:", error); } finally { setTablesLoading(false); } }; // 🔥 내부 DB 컬럼 로딩 const loadColumns = async (tableName: string) => { try { setColumnsLoading(true); const response = await tableTypeApi.getColumns(tableName); if (response && Array.isArray(response)) { const columnInfos: ColumnInfo[] = response.map((col: any) => ({ columnName: col.columnName || col.column_name, columnLabel: col.columnLabel || col.column_label, dataType: col.dataType || col.data_type || "text", isNullable: col.isNullable !== undefined ? col.isNullable : true, })); setTargetColumns(columnInfos); } } catch (error) { console.error("컬럼 로딩 실패:", error); setTargetColumns([]); } finally { setColumnsLoading(false); } }; const handleTableSelect = (tableName: string) => { const selectedTable = tables.find((t: any) => t.tableName === tableName); const label = (selectedTable as any)?.tableLabel || selectedTable?.displayName || tableName; setTargetTable(tableName); setSelectedTableLabel(label); setTablesOpen(false); updateNode(nodeId, { targetTable: tableName, displayName: label, }); }; const handleAddCondition = () => { const newConditions = [ ...whereConditions, { field: "", operator: "EQUALS", value: "", sourceField: undefined, staticValue: undefined, }, ]; setWhereConditions(newConditions); setFieldOpenState(new Array(newConditions.length).fill(false)); setSourceFieldsOpenState(new Array(newConditions.length).fill(false)); // 자동 저장 updateNode(nodeId, { whereConditions: newConditions, }); }; const handleRemoveCondition = (index: number) => { const newConditions = whereConditions.filter((_, i) => i !== index); setWhereConditions(newConditions); setFieldOpenState(new Array(newConditions.length).fill(false)); setSourceFieldsOpenState(new Array(newConditions.length).fill(false)); // 자동 저장 updateNode(nodeId, { whereConditions: newConditions, }); }; const handleConditionChange = (index: number, field: string, value: any) => { const newConditions = [...whereConditions]; newConditions[index] = { ...newConditions[index], [field]: value }; setWhereConditions(newConditions); // 자동 저장 updateNode(nodeId, { whereConditions: newConditions, }); }; const handleSave = () => { updateNode(nodeId, { displayName, targetTable, whereConditions, }); }; return (
{/* 경고 */}

위험한 작업입니다!

DELETE 작업은 되돌릴 수 없습니다. WHERE 조건을 반드시 설정하세요.

{/* 기본 정보 */}

기본 정보

setDisplayName(e.target.value)} className="mt-1" />
{/* 🔥 타겟 타입 선택 */}
{/* 내부 DB: 타겟 테이블 Combobox */} {targetType === "internal" && (
테이블을 찾을 수 없습니다. {tables.map((table: any) => ( handleTableSelect(table.tableName)} >
{table.tableLabel || table.displayName} {table.tableName}
))}
)} {/* 🔥 외부 DB 설정 */} {targetType === "external" && ( <>
{selectedExternalConnectionId && (
)} {externalTargetTable && externalColumns.length > 0 && (
{externalColumns.map((col) => (
{col.column_name} {col.data_type}
))}
)} )} {/* 🔥 REST API 설정 (DELETE는 간단함) */} {targetType === "api" && (
{ setApiEndpoint(e.target.value); updateNode(nodeId, { apiEndpoint: e.target.value }); }} className="h-8 text-xs" />
{apiAuthType !== "none" && (
{apiAuthType === "bearer" && ( { const newConfig = { token: e.target.value }; setApiAuthConfig(newConfig); updateNode(nodeId, { apiAuthConfig: newConfig }); }} className="h-8 text-xs" /> )} {apiAuthType === "basic" && (
{ const newConfig = { ...(apiAuthConfig as any), username: e.target.value }; setApiAuthConfig(newConfig); updateNode(nodeId, { apiAuthConfig: newConfig }); }} className="h-8 text-xs" /> { const newConfig = { ...(apiAuthConfig as any), password: e.target.value }; setApiAuthConfig(newConfig); updateNode(nodeId, { apiAuthConfig: newConfig }); }} className="h-8 text-xs" />
)} {apiAuthType === "apikey" && (
{ const newConfig = { ...(apiAuthConfig as any), headerName: e.target.value }; setApiAuthConfig(newConfig); updateNode(nodeId, { apiAuthConfig: newConfig }); }} className="h-8 text-xs" /> { const newConfig = { ...(apiAuthConfig as any), apiKey: e.target.value }; setApiAuthConfig(newConfig); updateNode(nodeId, { apiAuthConfig: newConfig }); }} className="h-8 text-xs" />
)}
)}
{Object.entries(apiHeaders).map(([key, value], index) => (
{ const newHeaders = { ...apiHeaders }; delete newHeaders[key]; newHeaders[e.target.value] = value; setApiHeaders(newHeaders); updateNode(nodeId, { apiHeaders: newHeaders }); }} className="h-7 flex-1 text-xs" /> { const newHeaders = { ...apiHeaders, [key]: e.target.value }; setApiHeaders(newHeaders); updateNode(nodeId, { apiHeaders: newHeaders }); }} className="h-7 flex-1 text-xs" />
))}
)}
{/* WHERE 조건 */}

WHERE 조건 (필수)

{/* 컬럼 로딩 상태 */} {targetType === "internal" && targetTable && columnsLoading && (
컬럼 정보를 불러오는 중...
)} {/* 테이블 미선택 안내 */} {targetType === "internal" && !targetTable && (
먼저 타겟 테이블을 선택하세요
)} {whereConditions.length > 0 ? (
{whereConditions.map((condition, index) => { // 현재 타입에 따라 사용 가능한 컬럼 리스트 결정 const availableColumns = targetType === "internal" ? targetColumns : targetType === "external" ? externalColumns.map((col) => ({ columnName: col.column_name, columnLabel: col.column_name, dataType: col.data_type, isNullable: true, })) : []; return (
조건 #{index + 1}
{/* 필드 - Combobox */}
{availableColumns.length > 0 ? ( { const newState = [...fieldOpenState]; newState[index] = open; setFieldOpenState(newState); }} > 필드를 찾을 수 없습니다. {availableColumns.map((col) => ( { handleConditionChange(index, "field", currentValue); const newState = [...fieldOpenState]; newState[index] = false; setFieldOpenState(newState); }} className="text-xs sm:text-sm" >
{col.columnLabel || col.columnName} {col.dataType}
))}
) : ( handleConditionChange(index, "field", e.target.value)} placeholder="조건 필드명" className="mt-1 h-8 text-xs" /> )}
{/* 🆕 소스 필드 - Combobox */}
{sourceFields.length > 0 ? ( { const newState = [...sourceFieldsOpenState]; newState[index] = open; setSourceFieldsOpenState(newState); }} > 필드를 찾을 수 없습니다. { handleConditionChange(index, "sourceField", undefined); const newState = [...sourceFieldsOpenState]; newState[index] = false; setSourceFieldsOpenState(newState); }} className="text-xs text-gray-400 sm:text-sm" > 없음 (정적 값 사용) {sourceFields.map((field) => ( { handleConditionChange(index, "sourceField", currentValue); const newState = [...sourceFieldsOpenState]; newState[index] = false; setSourceFieldsOpenState(newState); }} className="text-xs sm:text-sm" >
{field.label || field.name} {field.label && field.label !== field.name && ( {field.name} )}
))}
) : (
연결된 소스 노드가 없습니다
)}

소스 데이터에서 값을 가져올 필드

{/* 정적 값 */}
{ handleConditionChange(index, "staticValue", e.target.value || undefined); handleConditionChange(index, "value", e.target.value); }} placeholder="비교할 고정 값" className="mt-1 h-8 text-xs" />

소스 필드가 비어있을 때 사용됩니다

); })}
) : (
⚠️ WHERE 조건이 없습니다! 모든 데이터가 삭제됩니다!
)}
🚨 WHERE 조건 없이 삭제하면 테이블의 모든 데이터가 영구 삭제됩니다!
💡 실행 전 WHERE 조건을 꼭 확인하세요.
); }