"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Link, CheckCircle } from "lucide-react"; import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow"; import { ConnectionConfig, SimpleKeySettings, DataSaveSettings, SimpleExternalCallSettings, ConnectionSetupModalProps, } from "@/types/connectionTypes"; import { isConditionalConnection } from "@/utils/connectionUtils"; import { useConditionManager } from "@/hooks/useConditionManager"; import { ConditionalSettings } from "./condition/ConditionalSettings"; import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector"; import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings"; import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings"; import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings"; import { toast } from "sonner"; export const ConnectionSetupModal: React.FC = ({ isOpen, connection, companyCode, onConfirm, onCancel, }) => { const [config, setConfig] = useState({ relationshipName: "", connectionType: "simple-key", fromColumnName: "", toColumnName: "", settings: {}, }); // 연결 종류별 설정 상태 const [simpleKeySettings, setSimpleKeySettings] = useState({ notes: "", }); const [dataSaveSettings, setDataSaveSettings] = useState({ actions: [], }); const [externalCallSettings, setExternalCallSettings] = useState({ message: "", }); // 테이블 및 컬럼 선택을 위한 상태들 const [availableTables, setAvailableTables] = useState([]); const [selectedFromTable, setSelectedFromTable] = useState(""); const [selectedToTable, setSelectedToTable] = useState(""); const [fromTableColumns, setFromTableColumns] = useState([]); const [toTableColumns, setToTableColumns] = useState([]); const [selectedFromColumns, setSelectedFromColumns] = useState([]); const [selectedToColumns, setSelectedToColumns] = useState([]); const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({}); const [showSuccessModal, setShowSuccessModal] = useState(false); const [createdConnectionName, setCreatedConnectionName] = useState(""); const [pendingRelationshipData, setPendingRelationshipData] = useState(null); // 조건 관리 훅 사용 const { conditions, setConditions, addCondition, addGroupStart, addGroupEnd, updateCondition, removeCondition, getCurrentGroupLevel, } = useConditionManager(); // 기존 설정 로드 함수 const loadExistingSettings = useCallback( (settings: Record, connectionType: string) => { if (connectionType === "simple-key" && settings.notes) { setSimpleKeySettings({ notes: settings.notes as string, }); } else if (connectionType === "data-save") { // data-save 설정 로드 - 안전하게 처리 (다양한 구조 지원) let actionsData: Record[] = []; const settingsRecord = settings as Record; if (Array.isArray(settingsRecord.actions)) { // 직접 actions 배열이 있는 경우 actionsData = settingsRecord.actions as Record[]; } else if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) { // plan 객체 안에 actions가 있는 경우 const planRecord = settingsRecord.plan as Record; if (Array.isArray(planRecord.actions)) { actionsData = planRecord.actions as Record[]; } } else if (Array.isArray(settings)) { // settings 자체가 actions 배열인 경우 actionsData = settings as Record[]; } setDataSaveSettings({ actions: actionsData.map((action: Record) => ({ id: (action.id as string) || `action-${Date.now()}`, name: (action.name as string) || "새 액션", actionType: (action.actionType as "insert" | "update" | "delete" | "upsert") || "insert", conditions: Array.isArray(action.conditions) ? (action.conditions as ConditionNode[]).map((condition) => ({ ...condition, operator: condition.operator || "=", // 기본값 보장 })) : [], fieldMappings: Array.isArray(action.fieldMappings) ? action.fieldMappings.map((mapping: Record) => ({ sourceTable: (mapping.sourceTable as string) || "", sourceField: (mapping.sourceField as string) || "", targetTable: (mapping.targetTable as string) || "", targetField: (mapping.targetField as string) || "", defaultValue: (mapping.defaultValue as string) || "", transformFunction: (mapping.transformFunction as string) || "", })) : [], splitConfig: action.splitConfig ? { sourceField: ((action.splitConfig as Record).sourceField as string) || "", delimiter: ((action.splitConfig as Record).delimiter as string) || ",", targetField: ((action.splitConfig as Record).targetField as string) || "", } : undefined, })), }); // control 설정도 로드 (전체 실행 조건) if (settingsRecord.control && typeof settingsRecord.control === "object" && settingsRecord.control !== null) { const controlRecord = settingsRecord.control as Record; if (Array.isArray(controlRecord.conditionTree)) { const conditionTree = controlRecord.conditionTree as ConditionNode[]; setConditions( conditionTree.map((condition) => ({ ...condition, operator: condition.operator || "=", // 기본값 보장 })), ); } } } else if (connectionType === "external-call") { // 외부 호출 설정은 plan에서 로드 const settingsRecord = settings as Record; let externalCallData: Record = {}; if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) { const planRecord = settingsRecord.plan as Record; if (planRecord.externalCall && typeof planRecord.externalCall === "object") { externalCallData = planRecord.externalCall as Record; } } setExternalCallSettings({ configId: (externalCallData.configId as number) || undefined, configName: (externalCallData.configName as string) || undefined, message: (externalCallData.message as string) || "", }); } }, [setConditions, setSimpleKeySettings, setDataSaveSettings, setExternalCallSettings], ); // 테이블 목록 로드 useEffect(() => { const loadTables = async () => { try { const tables = await DataFlowAPI.getTables(); setAvailableTables(tables); } catch (error) { console.error("테이블 목록 로드 실패:", error); toast.error("테이블 목록을 불러오는데 실패했습니다."); } }; if (isOpen) { loadTables(); } }, [isOpen]); // 모달이 열릴 때 기본값 설정 useEffect(() => { if (isOpen && connection) { // 모달이 열릴 때마다 캐시 초기화 (라벨 업데이트 반영) setTableColumnsCache({}); const fromTableName = connection.fromNode.tableName; const toTableName = connection.toNode.tableName; const fromDisplayName = connection.fromNode.displayName; const toDisplayName = connection.toNode.displayName; // 테이블 선택 설정 setSelectedFromTable(fromTableName); setSelectedToTable(toTableName); // 기존 관계 정보가 있으면 사용, 없으면 기본값 설정 const existingRel = connection.existingRelationship; const connectionType = (existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key"; setConfig({ relationshipName: existingRel?.relationshipName || `${fromDisplayName} → ${toDisplayName}`, connectionType, fromColumnName: "", toColumnName: "", settings: existingRel?.settings || {}, }); // 기존 설정 데이터 로드 if (existingRel?.settings) { loadExistingSettings(existingRel.settings, connectionType); } else { // 기본값 설정 setSimpleKeySettings({ notes: existingRel?.note || `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`, }); setDataSaveSettings({ actions: [] }); } // 필드 선택 상태 초기화 setSelectedFromColumns([]); setSelectedToColumns([]); // 선택된 컬럼 정보가 있다면 설정 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, setConditions, loadExistingSettings]); // 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 loadTableColumns = async (tableName: string, forceReload = false): Promise => { if (tableColumnsCache[tableName] && !forceReload) { return tableColumnsCache[tableName]; } try { const columns = await DataFlowAPI.getTableColumns(tableName); setTableColumnsCache((prev) => ({ ...prev, [tableName]: columns, })); return columns; } catch (error) { console.error(`${tableName} 컬럼 로드 실패:`, error); return []; } }; // 테이블 선택 시 컬럼 로드 useEffect(() => { const loadColumns = async () => { const tablesToLoad = new Set(); // 필드 매핑에서 사용되는 모든 테이블 수집 dataSaveSettings.actions?.forEach((action) => { action.fieldMappings?.forEach((mapping) => { if (mapping.sourceTable && !tableColumnsCache[mapping.sourceTable]) { tablesToLoad.add(mapping.sourceTable); } if (mapping.targetTable && !tableColumnsCache[mapping.targetTable]) { tablesToLoad.add(mapping.targetTable); } }); }); // 필요한 테이블들의 컬럼만 로드 for (const tableName of tablesToLoad) { await loadTableColumns(tableName); } }; loadColumns(); }, [dataSaveSettings.actions, tableColumnsCache]); // eslint-disable-line react-hooks/exhaustive-deps const handleConfirm = () => { if (!config.relationshipName || !connection) { toast.error("필수 정보를 모두 입력해주세요."); return; } // 연결 종류별 설정을 준비 let settings = {}; let plan = {}; // plan 변수 선언 switch (config.connectionType) { case "simple-key": settings = simpleKeySettings; break; case "data-save": settings = dataSaveSettings; // INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증 for (const action of dataSaveSettings.actions) { if (action.actionType !== "insert") { if (!action.conditions || action.conditions.length === 0) { toast.error( `${action.actionType.toUpperCase()} 액션은 실행조건이 필수입니다. '${action.name}' 액션에 실행조건을 추가해주세요.`, ); return; } // 실제 조건이 있는지 확인 (group-start, group-end만 있는 경우 제외) const hasValidConditions = action.conditions.some((condition) => { if (condition.type !== "condition") return false; if (!condition.field || !condition.operator) return false; // value가 null, undefined, 빈 문자열이면 유효하지 않음 const value = condition.value; if (value === null || value === undefined || value === "") return false; return true; }); if (!hasValidConditions) { toast.error( `${action.actionType.toUpperCase()} 액션은 완전한 실행조건이 필요합니다. '${action.name}' 액션에 필드, 연산자, 값을 모두 설정해주세요.`, ); return; } } } break; case "external-call": // 외부 호출은 plan에 저장 plan = { externalCall: { configId: externalCallSettings.configId, configName: externalCallSettings.configName, message: externalCallSettings.message, }, }; settings = {}; // 외부 호출은 settings에 저장하지 않음 break; } // 단순 키값 연결일 때만 컬럼 선택 검증 if (config.connectionType === "simple-key") { if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) { toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요."); return; } } // 선택된 테이블과 컬럼 정보 사용 const fromTableName = selectedFromTable || connection.fromNode.tableName; const toTableName = selectedToTable || connection.toNode.tableName; // 조건부 연결 설정 데이터 준비 const conditionalSettings = isConditionalConnection(config.connectionType) ? { control: { triggerType: "insert", conditionTree: conditions.length > 0 ? conditions : null, }, category: { type: config.connectionType, }, plan: { sourceTable: fromTableName, targetActions: config.connectionType === "data-save" ? dataSaveSettings.actions.map((action) => ({ id: action.id, actionType: action.actionType, enabled: true, conditions: action.conditions?.map((condition) => { // 모든 조건 타입에 대해 operator 필드 보장 const baseCondition = { ...condition }; if (condition.type === "condition") { baseCondition.operator = condition.operator || "="; } return baseCondition; }) || [], fieldMappings: action.fieldMappings.map((mapping) => ({ sourceTable: mapping.sourceTable, sourceField: mapping.sourceField, targetTable: mapping.targetTable, targetField: mapping.targetField, defaultValue: mapping.defaultValue, transformFunction: mapping.transformFunction, })), splitConfig: action.splitConfig, })) : [], }, } : {}; // 컬럼 정보는 단순 키값 연결일 때만 사용 const finalFromColumns = config.connectionType === "simple-key" ? selectedFromColumns : []; const finalToColumns = config.connectionType === "simple-key" ? selectedToColumns : []; // 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달 const relationshipData: TableRelationship = { relationship_name: config.relationshipName, from_table_name: fromTableName, to_table_name: toTableName, from_column_name: finalFromColumns.join(","), // 여러 컬럼을 콤마로 구분 to_column_name: finalToColumns.join(","), // 여러 컬럼을 콤마로 구분 connection_type: config.connectionType, company_code: companyCode, settings: { ...settings, ...conditionalSettings, // 조건부 연결 설정 추가 ...plan, // 외부 호출 plan 추가 }, }; // 성공 모달 표시를 위한 상태 설정 setCreatedConnectionName(config.relationshipName); setPendingRelationshipData(relationshipData); setShowSuccessModal(true); }; const handleCancel = () => { setConfig({ relationshipName: "", connectionType: "simple-key", fromColumnName: "", toColumnName: "", }); onCancel(); }; const handleSuccessModalClose = () => { setShowSuccessModal(false); setCreatedConnectionName(""); // 저장된 관계 데이터를 부모에게 전달 if (pendingRelationshipData) { onConfirm(pendingRelationshipData); setPendingRelationshipData(null); } handleCancel(); // 원래 모달도 닫기 }; // 연결 종류별 설정 패널 렌더링 const renderConnectionTypeSettings = () => { switch (config.connectionType) { case "simple-key": return ( ); case "data-save": return ( ); case "external-call": return ( ); default: return null; } }; const isButtonDisabled = () => { // 공통 검증: 관계 이름은 필수 const hasRelationshipName = !!config.relationshipName?.trim(); if (!hasRelationshipName) return true; // 연결 타입별 검증 switch (config.connectionType) { case "simple-key": // 단순 키값 연결: From과 To 컬럼이 모두 선택되어야 함 const hasFromColumns = selectedFromColumns.length > 0; const hasToColumns = selectedToColumns.length > 0; return !hasFromColumns || !hasToColumns; case "data-save": // 데이터 저장: 액션과 필드 매핑이 완성되어야 함 const hasActions = dataSaveSettings.actions.length > 0; // DELETE 액션은 필드 매핑이 필요 없음 const allActionsHaveMappings = dataSaveSettings.actions.every((action) => { if (action.actionType === "delete") { return true; // DELETE는 필드 매핑 불필요 } return action.fieldMappings.length > 0; }); const allMappingsComplete = dataSaveSettings.actions.every((action) => { if (action.actionType === "delete") { return true; // DELETE는 필드 매핑 검증 생략 } // INSERT 액션의 경우 최소 하나의 매핑이 있으면 됨 (모든 컬럼 매핑 필수 조건 제거) if (action.actionType === "insert") { return true; // 필드 매핑이 있으면 충분함 } return action.fieldMappings.every((mapping) => { // 타겟은 항상 필요 if (!mapping.targetTable || !mapping.targetField) return false; // 소스와 기본값 중 하나는 있어야 함 const hasSource = mapping.sourceTable && mapping.sourceField; const hasDefault = mapping.defaultValue && mapping.defaultValue.trim(); // FROM 테이블이 비어있으면 기본값이 필요 if (!mapping.sourceTable) { return !!hasDefault; } // FROM 테이블이 있으면 소스 매핑 완성 또는 기본값 필요 return hasSource || hasDefault; }); }); // INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증 const allRequiredConditionsMet = dataSaveSettings.actions.every((action) => { if (action.actionType === "insert") { return true; // INSERT는 조건 불필요 } // INSERT가 아닌 액션은 유효한 조건이 있어야 함 if (!action.conditions || action.conditions.length === 0) { return false; } // 실제 조건이 있는지 확인 (group-start, group-end만 있는 경우 제외) const hasValidConditions = action.conditions.some((condition) => { if (condition.type !== "condition") return false; if (!condition.field || !condition.operator) return false; // value가 null, undefined, 빈 문자열이면 유효하지 않음 const value = condition.value; if (value === null || value === undefined || value === "") return false; return true; }); return hasValidConditions; }); return !hasActions || !allActionsHaveMappings || !allMappingsComplete || !allRequiredConditionsMet; case "external-call": // 외부 호출: 설정 ID와 메시지가 있어야 함 return !externalCallSettings.configId || !externalCallSettings.message?.trim(); default: return false; } }; if (!connection) return null; return ( <> 필드 연결 설정
{/* 기본 연결 설정 */}
setConfig({ ...config, relationshipName: e.target.value })} placeholder="employee_id_department_id_연결" className="text-sm" />
{/* 연결 종류 선택 */} {/* 조건부 연결을 위한 조건 설정 */} {isConditionalConnection(config.connectionType) && ( )} {/* 연결 종류별 상세 설정 */} {renderConnectionTypeSettings()}
연결 생성 완료 {createdConnectionName} 연결이 생성되었습니다.
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
확인
); };