diff --git a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx index 604b29f1..cec7c238 100644 --- a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx +++ b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx @@ -25,699 +25,11 @@ const initialState: DataConnectionState = { }, isLoading: false, validationErrors: [], +}; - // 컬럼 정보 초기화 - fromColumns: [], - toColumns: [], - ...initialData, - })); - - // 🔧 수정 모드 감지 (initialData에 diagramId가 있으면 수정 모드) - const diagramId = initialData?.diagramId; - - // 🔄 초기 데이터 로드 - useEffect(() => { - if (initialData && Object.keys(initialData).length > 1) { - console.log("🔄 초기 데이터 로드:", initialData); - - // 로드된 데이터로 state 업데이트 - setState((prev) => ({ - ...prev, - connectionType: initialData.connectionType || prev.connectionType, - - // 🔧 관계 정보 로드 - relationshipName: initialData.relationshipName || prev.relationshipName, - description: initialData.description || prev.description, - groupsLogicalOperator: initialData.groupsLogicalOperator || prev.groupsLogicalOperator, - - fromConnection: initialData.fromConnection || prev.fromConnection, - toConnection: initialData.toConnection || prev.toConnection, - fromTable: initialData.fromTable || prev.fromTable, - toTable: initialData.toTable || prev.toTable, - controlConditions: initialData.controlConditions || prev.controlConditions, - fieldMappings: initialData.fieldMappings || prev.fieldMappings, - - // 🔧 외부호출 설정 로드 - externalCallConfig: initialData.externalCallConfig || prev.externalCallConfig, - - // 🔧 액션 그룹 데이터 로드 (기존 호환성 포함) - actionGroups: - initialData.actionGroups || - // 기존 단일 액션 데이터를 그룹으로 변환 - (initialData.actionType || initialData.actionConditions - ? [ - { - id: "group_1", - name: "기본 액션 그룹", - logicalOperator: "AND" as const, - actions: [ - { - id: "action_1", - name: "액션 1", - actionType: initialData.actionType || ("insert" as const), - conditions: initialData.actionConditions || [], - fieldMappings: initialData.actionFieldMappings || [], - isEnabled: true, - }, - ], - isEnabled: true, - }, - ] - : prev.actionGroups), - - // 기존 호환성 필드들 - actionType: initialData.actionType || prev.actionType, - actionConditions: initialData.actionConditions || prev.actionConditions, - actionFieldMappings: initialData.actionFieldMappings || prev.actionFieldMappings, - - currentStep: initialData.fromConnection && initialData.toConnection ? 4 : 1, // 연결 정보가 있으면 4단계부터 시작 - })); - - console.log("✅ 초기 데이터 로드 완료"); - } - }, [initialData]); - - // 🎯 액션 핸들러들 - const actions: DataConnectionActions = { - // 연결 타입 설정 - setConnectionType: useCallback((type: "data_save" | "external_call") => { - console.log("🔄 [DataConnectionDesigner] setConnectionType 호출됨:", type); - setState((prev) => ({ - ...prev, - connectionType: type, - // 타입 변경 시 상태 초기화 - currentStep: 1, - fromConnection: undefined, - toConnection: undefined, - fromTable: undefined, - toTable: undefined, - fieldMappings: [], - validationErrors: [], - })); - toast.success(`연결 타입이 ${type === "data_save" ? "데이터 저장" : "외부 호출"}로 변경되었습니다.`); - }, []), - - // 🔧 관계 정보 설정 - setRelationshipName: useCallback((name: string) => { - setState((prev) => ({ - ...prev, - relationshipName: name, - })); - }, []), - - setDescription: useCallback((description: string) => { - setState((prev) => ({ - ...prev, - description: description, - })); - }, []), - - setGroupsLogicalOperator: useCallback((operator: "AND" | "OR") => { - setState((prev) => ({ ...prev, groupsLogicalOperator: operator })); - console.log("🔄 그룹 간 논리 연산자 변경:", operator); - }, []), - - // 단계 이동 - goToStep: useCallback((step: 1 | 2 | 3 | 4) => { - setState((prev) => ({ ...prev, currentStep: step })); - }, []), - - // 연결 선택 - selectConnection: useCallback((type: "from" | "to", connection: Connection) => { - setState((prev) => ({ - ...prev, - [type === "from" ? "fromConnection" : "toConnection"]: connection, - // 연결 변경 시 테이블과 매핑 초기화 - [type === "from" ? "fromTable" : "toTable"]: undefined, - fieldMappings: [], - })); - toast.success(`${type === "from" ? "소스" : "대상"} 연결이 선택되었습니다: ${connection.name}`); - }, []), - - // 테이블 선택 - selectTable: useCallback((type: "from" | "to", table: TableInfo) => { - setState((prev) => ({ - ...prev, - [type === "from" ? "fromTable" : "toTable"]: table, - // 테이블 변경 시 매핑과 컬럼 정보 초기화 - fieldMappings: [], - fromColumns: type === "from" ? [] : prev.fromColumns, - toColumns: type === "to" ? [] : prev.toColumns, - })); - toast.success( - `${type === "from" ? "소스" : "대상"} 테이블이 선택되었습니다: ${table.displayName || table.tableName}`, - ); - }, []), - - // 컬럼 정보 로드 (중앙 관리) - loadColumns: useCallback(async () => { - if (!state.fromConnection || !state.toConnection || !state.fromTable || !state.toTable) { - console.log("❌ 컬럼 로드: 필수 정보 누락"); - return; - } - - // 이미 로드된 경우 스킵 (배열 길이로 확인) - if (state.fromColumns && state.toColumns && state.fromColumns.length > 0 && state.toColumns.length > 0) { - console.log("✅ 컬럼 정보 이미 로드됨, 스킵", { - fromColumns: state.fromColumns.length, - toColumns: state.toColumns.length, - }); - return; - } - - console.log("🔄 중앙 컬럼 로드 시작:", { - from: `${state.fromConnection.id}/${state.fromTable.tableName}`, - to: `${state.toConnection.id}/${state.toTable.tableName}`, - }); - - setState((prev) => ({ - ...prev, - isLoading: true, - fromColumns: [], - toColumns: [], - })); - - try { - const [fromCols, toCols] = await Promise.all([ - getColumnsFromConnection(state.fromConnection.id, state.fromTable.tableName), - getColumnsFromConnection(state.toConnection.id, state.toTable.tableName), - ]); - - console.log("✅ 중앙 컬럼 로드 완료:", { - fromColumns: fromCols.length, - toColumns: toCols.length, - }); - - setState((prev) => ({ - ...prev, - fromColumns: Array.isArray(fromCols) ? fromCols : [], - toColumns: Array.isArray(toCols) ? toCols : [], - isLoading: false, - })); - } catch (error) { - console.error("❌ 중앙 컬럼 로드 실패:", error); - setState((prev) => ({ ...prev, isLoading: false })); - toast.error("컬럼 정보를 불러오는데 실패했습니다."); - } - }, [state.fromConnection, state.toConnection, state.fromTable, state.toTable, state.fromColumns, state.toColumns]), - - // 필드 매핑 생성 (호환성용 - 실제로는 각 액션에서 직접 관리) - createMapping: useCallback((fromField: ColumnInfo, toField: ColumnInfo) => { - const newMapping: FieldMapping = { - id: `${fromField.columnName}_to_${toField.columnName}_${Date.now()}`, - fromField, - toField, - isValid: true, - validationMessage: undefined, - }; - - setState((prev) => ({ - ...prev, - fieldMappings: [...prev.fieldMappings, newMapping], - })); - - console.log("🔗 전역 매핑 생성 (호환성):", { - newMapping, - fieldName: `${fromField.columnName} → ${toField.columnName}`, - }); - - toast.success(`매핑이 생성되었습니다: ${fromField.columnName} → ${toField.columnName}`); - }, []), - - // 필드 매핑 업데이트 - updateMapping: useCallback((mappingId: string, updates: Partial) => { - setState((prev) => ({ - ...prev, - fieldMappings: prev.fieldMappings.map((mapping) => - mapping.id === mappingId ? { ...mapping, ...updates } : mapping, - ), - })); - }, []), - - // 필드 매핑 삭제 (호환성용 - 실제로는 각 액션에서 직접 관리) - deleteMapping: useCallback((mappingId: string) => { - setState((prev) => ({ - ...prev, - fieldMappings: prev.fieldMappings.filter((mapping) => mapping.id !== mappingId), - })); - - console.log("🗑️ 전역 매핑 삭제 (호환성):", { mappingId }); - toast.success("매핑이 삭제되었습니다."); - }, []), - - // 매핑 검증 - validateMappings: useCallback(async (): Promise => { - setState((prev) => ({ ...prev, isLoading: true })); - - try { - // TODO: 실제 검증 로직 구현 - const result: ValidationResult = { - isValid: true, - errors: [], - warnings: [], - }; - - setState((prev) => ({ - ...prev, - validationErrors: result.errors, - isLoading: false, - })); - - return result; - } catch (error) { - setState((prev) => ({ ...prev, isLoading: false })); - throw error; - } - }, []), - - // 제어 조건 관리 (전체 실행 조건) - addControlCondition: useCallback(() => { - setState((prev) => ({ - ...prev, - controlConditions: [ - ...prev.controlConditions, - { - id: Date.now().toString(), - type: "condition", - field: "", - operator: "=", - value: "", - dataType: "string", - }, - ], - })); - }, []), - - updateControlCondition: useCallback((index: number, condition: any) => { - setState((prev) => ({ - ...prev, - controlConditions: prev.controlConditions.map((cond, i) => (i === index ? { ...cond, ...condition } : cond)), - })); - }, []), - - deleteControlCondition: useCallback((index: number) => { - setState((prev) => ({ - ...prev, - controlConditions: prev.controlConditions.filter((_, i) => i !== index), - })); - toast.success("제어 조건이 삭제되었습니다."); - }, []), - - // 외부호출 설정 업데이트 - updateExternalCallConfig: useCallback((config: any) => { - console.log("🔄 외부호출 설정 업데이트:", config); - setState((prev) => ({ - ...prev, - externalCallConfig: config, - })); - }, []), - - // 액션 설정 관리 - setActionType: useCallback((type: "insert" | "update" | "delete" | "upsert") => { - setState((prev) => ({ - ...prev, - actionType: type, - // INSERT가 아닌 경우 조건 초기화 - actionConditions: type === "insert" ? [] : prev.actionConditions, - })); - toast.success(`액션 타입이 ${type.toUpperCase()}로 변경되었습니다.`); - }, []), - - addActionCondition: useCallback(() => { - setState((prev) => ({ - ...prev, - actionConditions: [ - ...prev.actionConditions, - { - id: Date.now().toString(), - type: "condition", - field: "", - operator: "=", - value: "", - dataType: "string", - }, - ], - })); - }, []), - - updateActionCondition: useCallback((index: number, condition: any) => { - setState((prev) => ({ - ...prev, - actionConditions: prev.actionConditions.map((cond, i) => (i === index ? { ...cond, ...condition } : cond)), - })); - }, []), - - // 🔧 액션 조건 배열 전체 업데이트 (ActionConditionBuilder용) - setActionConditions: useCallback((conditions: any[]) => { - setState((prev) => ({ - ...prev, - actionConditions: conditions, - })); - }, []), - - deleteActionCondition: useCallback((index: number) => { - setState((prev) => ({ - ...prev, - actionConditions: prev.actionConditions.filter((_, i) => i !== index), - })); - toast.success("조건이 삭제되었습니다."); - }, []), - - // 🎯 액션 그룹 관리 (멀티 액션) - addActionGroup: useCallback(() => { - const newGroupId = `group_${Date.now()}`; - setState((prev) => ({ - ...prev, - actionGroups: [ - ...prev.actionGroups, - { - id: newGroupId, - name: `액션 그룹 ${prev.actionGroups.length + 1}`, - logicalOperator: "AND" as const, - actions: [ - { - id: `action_${Date.now()}`, - name: "액션 1", - actionType: "insert" as const, - conditions: [], - fieldMappings: [], - isEnabled: true, - }, - ], - isEnabled: true, - }, - ], - })); - toast.success("새 액션 그룹이 추가되었습니다."); - }, []), - - updateActionGroup: useCallback((groupId: string, updates: Partial) => { - setState((prev) => ({ - ...prev, - actionGroups: prev.actionGroups.map((group) => (group.id === groupId ? { ...group, ...updates } : group)), - })); - }, []), - - deleteActionGroup: useCallback((groupId: string) => { - setState((prev) => ({ - ...prev, - actionGroups: prev.actionGroups.filter((group) => group.id !== groupId), - })); - toast.success("액션 그룹이 삭제되었습니다."); - }, []), - - addActionToGroup: useCallback((groupId: string) => { - const newActionId = `action_${Date.now()}`; - setState((prev) => ({ - ...prev, - actionGroups: prev.actionGroups.map((group) => - group.id === groupId - ? { - ...group, - actions: [ - ...group.actions, - { - id: newActionId, - name: `액션 ${group.actions.length + 1}`, - actionType: "insert" as const, - conditions: [], - fieldMappings: [], - isEnabled: true, - }, - ], - } - : group, - ), - })); - toast.success("새 액션이 추가되었습니다."); - }, []), - - updateActionInGroup: useCallback((groupId: string, actionId: string, updates: Partial) => { - setState((prev) => ({ - ...prev, - actionGroups: prev.actionGroups.map((group) => - group.id === groupId - ? { - ...group, - actions: group.actions.map((action) => (action.id === actionId ? { ...action, ...updates } : action)), - } - : group, - ), - })); - }, []), - - deleteActionFromGroup: useCallback((groupId: string, actionId: string) => { - setState((prev) => ({ - ...prev, - actionGroups: prev.actionGroups.map((group) => - group.id === groupId - ? { - ...group, - actions: group.actions.filter((action) => action.id !== actionId), - } - : group, - ), - })); - toast.success("액션이 삭제되었습니다."); - }, []), - - // 매핑 저장 (직접 저장) - saveMappings: useCallback(async () => { - // 관계명과 설명이 없으면 저장할 수 없음 - if (!state.relationshipName?.trim()) { - toast.error("관계 이름을 입력해주세요."); - actions.goToStep(1); // 첫 번째 단계로 이동 - return; - } - - // 외부호출인 경우 API URL만 확인 (테이블 검증 제외) - if (state.connectionType === "external_call") { - if (!state.externalCallConfig?.restApiSettings?.apiUrl) { - toast.error("API URL을 입력해주세요."); - return; - } - // 외부호출은 테이블 정보 검증 건너뛰기 - } - - // 중복 체크 (수정 모드가 아닌 경우에만) - if (!diagramId) { - try { - const duplicateCheck = await checkRelationshipNameDuplicate(state.relationshipName, diagramId); - if (duplicateCheck.isDuplicate) { - toast.error(`"${state.relationshipName}" 이름이 이미 사용 중입니다. 다른 이름을 사용해주세요.`); - actions.goToStep(1); // 첫 번째 단계로 이동 - return; - } - } catch (error) { - console.error("중복 체크 실패:", error); - toast.error("관계명 중복 체크 중 오류가 발생했습니다."); - return; - } - } - - setState((prev) => ({ ...prev, isLoading: true })); - - try { - // 실제 저장 로직 구현 - connectionType에 따라 필요한 설정만 포함 - let saveData: any = { - relationshipName: state.relationshipName, - description: state.description, - connectionType: state.connectionType, - }; - - if (state.connectionType === "external_call") { - // 외부호출 타입인 경우: 외부호출 설정만 포함 - console.log("💾 외부호출 타입 저장 - 외부호출 설정만 포함"); - saveData = { - ...saveData, - // 외부호출 관련 설정만 포함 - externalCallConfig: state.externalCallConfig, - actionType: "external_call", - // 데이터 저장 관련 설정은 제외 (null/빈 배열로 설정) - fromConnection: null, - toConnection: null, - fromTable: null, - toTable: null, - actionGroups: [], - controlConditions: [], - actionConditions: [], - fieldMappings: [], - }; - } else if (state.connectionType === "data_save") { - // 데이터 저장 타입인 경우: 데이터 저장 설정만 포함 - console.log("💾 데이터 저장 타입 저장 - 데이터 저장 설정만 포함"); - saveData = { - ...saveData, - // 데이터 저장 관련 설정만 포함 - fromConnection: state.fromConnection, - toConnection: state.toConnection, - fromTable: state.fromTable, - toTable: state.toTable, - actionGroups: state.actionGroups, - groupsLogicalOperator: state.groupsLogicalOperator, - controlConditions: state.controlConditions, - // 기존 호환성을 위한 필드들 (첫 번째 액션 그룹의 첫 번째 액션에서 추출) - actionType: state.actionGroups[0]?.actions[0]?.actionType || state.actionType || "insert", - actionConditions: state.actionGroups[0]?.actions[0]?.conditions || state.actionConditions || [], - fieldMappings: state.actionGroups[0]?.actions[0]?.fieldMappings || state.fieldMappings || [], - // 외부호출 관련 설정은 제외 (null로 설정) - externalCallConfig: null, - }; - } - - console.log("💾 직접 저장 시작:", { saveData, diagramId, isEdit: !!diagramId }); - - // 데이터 저장 타입인 경우 기존 외부호출 설정 정리 - if (state.connectionType === "data_save" && diagramId) { - console.log("🧹 데이터 저장 타입으로 변경 - 기존 외부호출 설정 정리"); - try { - const { ExternalCallConfigAPI } = await import("@/lib/api/externalCallConfig"); - - // 기존 외부호출 설정이 있는지 확인하고 삭제 또는 비활성화 - const existingConfigs = await ExternalCallConfigAPI.getConfigs({ - company_code: "*", - is_active: "Y", - }); - - const existingConfig = existingConfigs.data?.find( - (config: any) => config.config_name === (state.relationshipName || "외부호출 설정"), - ); - - if (existingConfig) { - console.log("🗑️ 기존 외부호출 설정 비활성화:", existingConfig.id); - // 설정을 비활성화 (삭제하지 않고 is_active를 'N'으로 변경) - await ExternalCallConfigAPI.updateConfig(existingConfig.id, { - ...existingConfig, - is_active: "N", - updated_at: new Date().toISOString(), - }); - } - } catch (cleanupError) { - console.warn("⚠️ 외부호출 설정 정리 실패 (무시하고 계속):", cleanupError); - } - } - - // 외부호출인 경우에만 external-call-configs에 설정 저장 - if (state.connectionType === "external_call" && state.externalCallConfig) { - try { - const { ExternalCallConfigAPI } = await import("@/lib/api/externalCallConfig"); - - const configData = { - config_name: state.relationshipName || "외부호출 설정", - call_type: "rest-api", - api_type: "generic", - config_data: state.externalCallConfig.restApiSettings, - description: state.description || "", - company_code: "*", // 기본값 - }; - - let configResult; - - if (diagramId) { - // 수정 모드: 기존 설정이 있는지 확인하고 업데이트 또는 생성 - console.log("🔄 수정 모드 - 외부호출 설정 처리"); - - try { - // 먼저 기존 설정 조회 시도 - const existingConfigs = await ExternalCallConfigAPI.getConfigs({ - company_code: "*", - is_active: "Y", - }); - - const existingConfig = existingConfigs.data?.find( - (config: any) => config.config_name === (state.relationshipName || "외부호출 설정"), - ); - - if (existingConfig) { - // 기존 설정 업데이트 - console.log("📝 기존 외부호출 설정 업데이트:", existingConfig.id); - configResult = await ExternalCallConfigAPI.updateConfig(existingConfig.id, configData); - } else { - // 기존 설정이 없으면 새로 생성 - console.log("🆕 새 외부호출 설정 생성 (수정 모드)"); - configResult = await ExternalCallConfigAPI.createConfig(configData); - } - } catch (updateError) { - // 중복 생성 오류인 경우 무시하고 계속 진행 - if (updateError.message && updateError.message.includes("이미 존재합니다")) { - console.log("⚠️ 외부호출 설정이 이미 존재함 - 기존 설정 사용"); - configResult = { success: true, message: "기존 외부호출 설정 사용" }; - } else { - console.warn("⚠️ 외부호출 설정 처리 실패:", updateError); - throw updateError; - } - } - } else { - // 신규 생성 모드 - console.log("🆕 신규 생성 모드 - 외부호출 설정 생성"); - try { - configResult = await ExternalCallConfigAPI.createConfig(configData); - } catch (createError) { - // 중복 생성 오류인 경우 무시하고 계속 진행 - if (createError.message && createError.message.includes("이미 존재합니다")) { - console.log("⚠️ 외부호출 설정이 이미 존재함 - 기존 설정 사용"); - configResult = { success: true, message: "기존 외부호출 설정 사용" }; - } else { - throw createError; - } - } - } - - if (!configResult.success) { - throw new Error(configResult.error || "외부호출 설정 저장 실패"); - } - - console.log("✅ 외부호출 설정 저장 완료:", configResult.data); - } catch (configError) { - console.error("❌ 외부호출 설정 저장 실패:", configError); - // 외부호출 설정 저장 실패해도 관계는 저장하도록 함 - toast.error("외부호출 설정 저장에 실패했지만 관계는 저장되었습니다."); - } - } - - // 백엔드 API 호출 (수정 모드인 경우 diagramId 전달) - const result = await saveDataflowRelationship(saveData, diagramId); - - console.log("✅ 저장 완료:", result); - - setState((prev) => ({ ...prev, isLoading: false })); - toast.success(`"${state.relationshipName}" 관계가 성공적으로 저장되었습니다.`); - - // 저장 후 닫기 - if (onClose) { - onClose(); - } - } catch (error: any) { - console.error("❌ 저장 실패:", error); - setState((prev) => ({ ...prev, isLoading: false })); - toast.error(error.message || "저장 중 오류가 발생했습니다."); - } - }, [state, diagramId, onClose]), - - // 테스트 실행 - testExecution: useCallback(async (): Promise => { - setState((prev) => ({ ...prev, isLoading: true })); - - try { - // TODO: 실제 테스트 로직 구현 - const result: TestResult = { - success: true, - message: "테스트가 성공적으로 완료되었습니다.", - affectedRows: 10, - executionTime: 250, - }; - - setState((prev) => ({ ...prev, isLoading: false })); - toast.success(result.message); - - return result; - } catch (error) { - setState((prev) => ({ ...prev, isLoading: false })); - toast.error("테스트 실행 중 오류가 발생했습니다."); - throw error; - } - }, []), - }; +export const DataConnectionDesigner: React.FC = () => { + const [state, setState] = useState(initialState); + const { isMobile, isTablet } = useResponsive(); return (
@@ -746,18 +58,52 @@ const initialState: DataConnectionState = { />
- )} - {/* 메인 컨텐츠 - 좌우 분할 레이아웃 */} -
- {/* 좌측 패널 (30%) - 항상 표시 */} -
- +
+ setState(prev => ({ ...prev, currentStep: step }))} + /> + +
+ {state.currentStep === 1 && ( + setState(prev => ({ ...prev, fromConnection: conn }))} + onToConnectionChange={(conn) => setState(prev => ({ ...prev, toConnection: conn }))} + onNext={() => setState(prev => ({ ...prev, currentStep: 2 }))} + /> + )} + + {state.currentStep === 2 && ( + setState(prev => ({ ...prev, fromTable: table }))} + onToTableChange={(table) => setState(prev => ({ ...prev, toTable: table }))} + onNext={() => setState(prev => ({ ...prev, currentStep: 3 }))} + onBack={() => setState(prev => ({ ...prev, currentStep: 1 }))} + /> + )} + + {state.currentStep === 3 && ( + setState(prev => ({ ...prev, fieldMappings: mappings }))} + onBack={() => setState(prev => ({ ...prev, currentStep: 2 }))} + onSave={() => { + // 저장 로직 + console.log("저장:", state); + alert("데이터 연결 설정이 저장되었습니다!"); + }} + /> + )}
- - {/* 우측 패널 (80%) */} -
-