"use client"; import React, { useState, useCallback, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { X, ArrowLeft } from "lucide-react"; // API import import { saveDataflowRelationship, checkRelationshipNameDuplicate } from "@/lib/api/dataflowSave"; import { getColumnsFromConnection } from "@/lib/api/multiConnection"; // 타입 import import { DataConnectionState, DataConnectionActions, DataConnectionDesignerProps, FieldMapping, ValidationResult, TestResult, MappingStats, ActionGroup, SingleAction, } from "./types/redesigned"; import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection"; // 컴포넌트 import import LeftPanel from "./LeftPanel/LeftPanel"; import RightPanel from "./RightPanel/RightPanel"; /** * 🎨 데이터 연결 설정 메인 디자이너 * - 좌우 분할 레이아웃 (30% + 70%) * - 상태 관리 및 액션 처리 * - 기존 모달 기능을 메인 화면으로 통합 */ const DataConnectionDesigner: React.FC = ({ onClose, initialData, showBackButton = false, }) => { // 🔄 상태 관리 const [state, setState] = useState(() => ({ connectionType: "data_save", currentStep: 1, fieldMappings: [], mappingStats: { totalMappings: 0, validMappings: 0, invalidMappings: 0, missingRequiredFields: 0, estimatedRows: 0, actionType: "INSERT", }, // 제어 실행 조건 초기값 controlConditions: [], // 액션 그룹 초기값 (멀티 액션) actionGroups: [ { id: "group_1", name: "기본 액션 그룹", logicalOperator: "AND" as const, actions: [ { id: "action_1", name: "액션 1", actionType: "insert" as const, conditions: [], fieldMappings: [], isEnabled: true, }, ], isEnabled: true, }, ], groupsLogicalOperator: "AND" as "AND" | "OR", // 기존 호환성 필드들 (deprecated) actionType: "insert", actionConditions: [], actionFieldMappings: [], 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 { // 실제 저장 로직 구현 const saveData = { relationshipName: state.relationshipName, description: state.description, connectionType: state.connectionType, // 외부호출인 경우 테이블 정보는 선택사항 fromConnection: state.connectionType === "external_call" ? null : state.fromConnection, toConnection: state.connectionType === "external_call" ? null : state.toConnection, fromTable: state.connectionType === "external_call" ? null : state.fromTable, toTable: state.connectionType === "external_call" ? null : state.toTable, // 🔧 멀티 액션 그룹 데이터 포함 actionGroups: state.connectionType === "external_call" ? [] : state.actionGroups, groupsLogicalOperator: state.groupsLogicalOperator, // 외부호출 설정 포함 externalCallConfig: state.externalCallConfig, // 기존 호환성을 위한 필드들 (첫 번째 액션 그룹의 첫 번째 액션에서 추출) actionType: state.connectionType === "external_call" ? "external_call" : state.actionGroups[0]?.actions[0]?.actionType || state.actionType || "insert", controlConditions: state.connectionType === "external_call" ? [] : state.controlConditions, actionConditions: state.connectionType === "external_call" ? [] : state.actionGroups[0]?.actions[0]?.conditions || state.actionConditions || [], fieldMappings: state.connectionType === "external_call" ? [] : state.actionGroups[0]?.actions[0]?.fieldMappings || state.fieldMappings || [], }; console.log("💾 직접 저장 시작:", { saveData, diagramId, isEdit: !!diagramId }); // 외부호출인 경우 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: "*", // 기본값 }; const configResult = await ExternalCallConfigAPI.createConfig(configData); 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; } }, []), }; return (
{/* 상단 네비게이션 */} {showBackButton && (

🔗 데이터 연결 설정

{state.connectionType === "data_save" ? "데이터 저장" : "외부 호출"} 연결 설정

)} {/* 메인 컨텐츠 - 좌우 분할 레이아웃 */}
{/* 좌측 패널 (30%) - 항상 표시 */}
{/* 우측 패널 (70%) */}
); }; export default DataConnectionDesigner;