diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 5f3a962b..f9e7cbb5 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -92,7 +92,7 @@ app.use( // Rate Limiting (개발 환경에서는 완화) const limiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1분 - max: config.nodeEnv === "development" ? 10000 : 1000, // 개발환경에서는 10000으로 증가, 운영환경에서는 100 + max: config.nodeEnv === "development" ? 10000 : 100, // 개발환경에서는 10000으로 증가, 운영환경에서는 100 message: { error: "너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.", }, diff --git a/frontend/components/dataflow/external-call/DataMappingSettings.tsx b/frontend/components/dataflow/external-call/DataMappingSettings.tsx index a21a974c..a4e1ea56 100644 --- a/frontend/components/dataflow/external-call/DataMappingSettings.tsx +++ b/frontend/components/dataflow/external-call/DataMappingSettings.tsx @@ -30,6 +30,7 @@ interface DataMappingSettingsProps { httpMethod: string; availableTables?: TableInfo[]; readonly?: boolean; + tablesLoading?: boolean; } export const DataMappingSettings: React.FC = ({ @@ -38,6 +39,7 @@ export const DataMappingSettings: React.FC = ({ httpMethod, availableTables = [], readonly = false, + tablesLoading = false, }) => { const [localConfig, setLocalConfig] = useState(config); @@ -228,17 +230,27 @@ export const DataMappingSettings: React.FC = ({ diff --git a/frontend/components/dataflow/external-call/ExternalCallPanel.tsx b/frontend/components/dataflow/external-call/ExternalCallPanel.tsx index d4a3204e..ea8add8d 100644 --- a/frontend/components/dataflow/external-call/ExternalCallPanel.tsx +++ b/frontend/components/dataflow/external-call/ExternalCallPanel.tsx @@ -17,6 +17,10 @@ import { } from "@/types/external-call/ExternalCallTypes"; import { DataMappingConfig, TableInfo } from "@/types/external-call/DataMappingTypes"; +// API import +import { DataFlowAPI } from "@/lib/api/dataflow"; +import { toast } from "sonner"; + // 하위 컴포넌트 import import RestApiSettings from "./RestApiSettings"; import ExternalCallTestPanel from "./ExternalCallTestPanel"; @@ -41,8 +45,14 @@ const ExternalCallPanel: React.FC = ({ }); // 상태 관리 const [config, setConfig] = useState( - () => - initialSettings || { + () => { + if (initialSettings) { + console.log("🔄 [ExternalCallPanel] 기존 설정 로드:", initialSettings); + return initialSettings; + } + + console.log("🔄 [ExternalCallPanel] 기본 설정 사용"); + return { callType: "rest-api", restApiSettings: { apiUrl: "", @@ -63,7 +73,8 @@ const ExternalCallPanel: React.FC = ({ timeout: 30000, // 30초 retryCount: 3, }, - }, + }; + }, ); const [activeTab, setActiveTab] = useState("settings"); @@ -71,48 +82,69 @@ const ExternalCallPanel: React.FC = ({ const [isConfigValid, setIsConfigValid] = useState(false); // 데이터 매핑 상태 - const [dataMappingConfig, setDataMappingConfig] = useState(() => ({ - direction: "none", - })); + const [dataMappingConfig, setDataMappingConfig] = useState(() => { + // initialSettings에서 데이터 매핑 정보 불러오기 + if (initialSettings?.dataMappingConfig) { + console.log("🔄 [ExternalCallPanel] 기존 데이터 매핑 설정 로드:", initialSettings.dataMappingConfig); + return initialSettings.dataMappingConfig; + } + + console.log("🔄 [ExternalCallPanel] 기본 데이터 매핑 설정 사용"); + return { + direction: "none", + }; + }); - // 사용 가능한 테이블 목록 (임시 데이터) - const [availableTables] = useState([ - { - name: "customers", - displayName: "고객", - fields: [ - { name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, - { name: "name", dataType: "string", nullable: false }, - { name: "email", dataType: "string", nullable: true }, - { name: "phone", dataType: "string", nullable: true }, - { name: "created_at", dataType: "date", nullable: false }, - ], - }, - { - name: "orders", - displayName: "주문", - fields: [ - { name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, - { name: "customer_id", dataType: "number", nullable: false }, - { name: "product_name", dataType: "string", nullable: false }, - { name: "quantity", dataType: "number", nullable: false }, - { name: "price", dataType: "number", nullable: false }, - { name: "status", dataType: "string", nullable: false }, - { name: "order_date", dataType: "date", nullable: false }, - ], - }, - { - name: "products", - displayName: "제품", - fields: [ - { name: "id", dataType: "number", nullable: false, isPrimaryKey: true }, - { name: "name", dataType: "string", nullable: false }, - { name: "price", dataType: "number", nullable: false }, - { name: "stock", dataType: "number", nullable: false }, - { name: "category", dataType: "string", nullable: true }, - ], - }, - ]); + // 사용 가능한 테이블 목록 (실제 API에서 로드) + const [availableTables, setAvailableTables] = useState([]); + const [tablesLoading, setTablesLoading] = useState(false); + + // 테이블 목록 로드 + useEffect(() => { + const loadTables = async () => { + try { + setTablesLoading(true); + const tables = await DataFlowAPI.getTables(); + + // 테이블 정보를 TableInfo 형식으로 변환 + const tableInfos: TableInfo[] = await Promise.all( + tables.map(async (table) => { + try { + const columns = await DataFlowAPI.getTableColumns(table.tableName); + return { + name: table.tableName, + displayName: table.displayName || table.tableName, + fields: columns.map((col) => ({ + name: col.columnName, + dataType: col.dataType, + nullable: col.nullable, + isPrimaryKey: col.isPrimaryKey || false, + })), + }; + } catch (error) { + console.warn(`테이블 ${table.tableName} 컬럼 정보 로드 실패:`, error); + return { + name: table.tableName, + displayName: table.displayName || table.tableName, + fields: [], + }; + } + }) + ); + + setAvailableTables(tableInfos); + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + toast.error("테이블 목록을 불러오는데 실패했습니다."); + // 실패 시 빈 배열로 설정 + setAvailableTables([]); + } finally { + setTablesLoading(false); + } + }; + + loadTables(); + }, []); // 설정 변경 핸들러 const handleRestApiSettingsChange = useCallback( @@ -136,6 +168,22 @@ const ExternalCallPanel: React.FC = ({ [config, onSettingsChange, dataMappingConfig], ); + // 데이터 매핑 설정 변경 핸들러 + const handleDataMappingConfigChange = useCallback( + (newMappingConfig: DataMappingConfig) => { + console.log("🔄 [ExternalCallPanel] 데이터 매핑 설정 변경:", newMappingConfig); + + setDataMappingConfig(newMappingConfig); + + // 전체 설정에 데이터 매핑 정보 포함하여 상위로 전달 + onSettingsChange({ + ...config, + dataMappingConfig: newMappingConfig, + }); + }, + [config, onSettingsChange], + ); + // 테스트 결과 핸들러 const handleTestResult = useCallback((result: ApiTestResult) => { setLastTestResult(result); @@ -225,10 +273,11 @@ const ExternalCallPanel: React.FC = ({ diff --git a/frontend/lib/utils/improvedButtonActionExecutor.ts b/frontend/lib/utils/improvedButtonActionExecutor.ts index c9c38be4..6c02172e 100644 --- a/frontend/lib/utils/improvedButtonActionExecutor.ts +++ b/frontend/lib/utils/improvedButtonActionExecutor.ts @@ -271,6 +271,14 @@ export class ImprovedButtonActionExecutor { // 2. 관계 타입에 따른 실행 const relationships = relationshipData.relationships; const connectionType = relationships.connectionType; + + console.log(`🔍 관계 상세 정보:`, { + connectionType, + hasExternalCallConfig: !!relationships.externalCallConfig, + externalCallConfig: relationships.externalCallConfig, + hasDataSaveConfig: !!relationships.dataSaveConfig, + dataSaveConfig: relationships.dataSaveConfig, + }); let result: ExecutionResult; @@ -339,8 +347,13 @@ export class ImprovedButtonActionExecutor { context: ButtonExecutionContext ): Promise { try { + console.log(`🔍 외부 호출 실행 시작 - relationships 구조:`, relationships); + const externalCallConfig = relationships.externalCallConfig; + console.log(`🔍 externalCallConfig:`, externalCallConfig); + if (!externalCallConfig) { + console.error('❌ 외부 호출 설정이 없습니다. relationships 구조:', relationships); throw new Error('외부 호출 설정이 없습니다'); } @@ -394,7 +407,7 @@ export class ImprovedButtonActionExecutor { timeout: restApiSettings.timeout || 30000, retryCount: restApiSettings.retryCount || 3, }, - templateData: restApiSettings.httpMethod !== 'GET' ? JSON.parse(requestBody) : {}, + templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData, }; console.log(`📤 백엔드로 전송할 데이터:`, requestPayload); @@ -412,11 +425,17 @@ export class ImprovedButtonActionExecutor { // 데이터 매핑 처리 (inbound mapping) if (externalCallConfig.dataMappingConfig?.inboundMapping) { + console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`); + console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping); + console.log(`📥 응답 데이터:`, responseData); + await this.processInboundMapping( externalCallConfig.dataMappingConfig.inboundMapping, responseData, context ); + } else { + console.log(`ℹ️ 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`); } return { @@ -596,28 +615,24 @@ export class ImprovedButtonActionExecutor { connection?: any ): Promise { try { - // 데이터 저장 API 호출 - const response = await fetch('/api/dataflow/execute-data-action', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}`, - }, - body: JSON.stringify({ - tableName, - data, - actionType, - connection, - }), + console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, { + actionType, + data, + connection }); - if (!response.ok) { - throw new Error(`데이터 저장 API 호출 실패: ${response.status}`); - } + // 데이터 저장 API 호출 (apiClient 사용) + const response = await apiClient.post('/dataflow/execute-data-action', { + tableName, + data, + actionType, + connection, + }); - return await response.json(); + console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data); + return response.data; } catch (error) { - console.error('데이터 저장 오류:', error); + console.error('테이블 데이터 저장 오류:', error); throw error; } } @@ -670,6 +685,111 @@ export class ImprovedButtonActionExecutor { return true; } + /** + * 다양한 API 응답 구조에서 실제 데이터 추출 + */ + private static extractActualData(responseData: any): any { + console.log(`🔍 데이터 추출 시작 - 원본 타입: ${typeof responseData}`); + + // null이나 undefined인 경우 + if (!responseData) { + console.log(`⚠️ 응답 데이터가 null 또는 undefined`); + return []; + } + + // 이미 배열인 경우 (직접 배열 응답) + if (Array.isArray(responseData)) { + console.log(`✅ 직접 배열 응답 감지`); + return responseData; + } + + // 문자열인 경우 JSON 파싱 시도 + if (typeof responseData === 'string') { + console.log(`🔄 JSON 문자열 파싱 시도`); + try { + const parsed = JSON.parse(responseData); + console.log(`✅ JSON 파싱 성공, 재귀 호출`); + return this.extractActualData(parsed); + } catch (error) { + console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error); + return [responseData]; + } + } + + // 객체가 아닌 경우 (숫자 등) + if (typeof responseData !== 'object') { + console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`); + return [responseData]; + } + + // 일반적인 데이터 필드명들을 우선순위대로 확인 + const commonDataFields = [ + 'data', // { data: [...] } + 'result', // { result: [...] } + 'results', // { results: [...] } + 'items', // { items: [...] } + 'list', // { list: [...] } + 'records', // { records: [...] } + 'rows', // { rows: [...] } + 'content', // { content: [...] } + 'payload', // { payload: [...] } + 'response', // { response: [...] } + ]; + + for (const field of commonDataFields) { + if (responseData[field] !== undefined) { + console.log(`✅ '${field}' 필드에서 데이터 추출`); + + const extractedData = responseData[field]; + + // 추출된 데이터가 문자열인 경우 JSON 파싱 시도 + if (typeof extractedData === 'string') { + console.log(`🔄 추출된 데이터가 JSON 문자열, 파싱 시도`); + try { + const parsed = JSON.parse(extractedData); + console.log(`✅ JSON 파싱 성공, 재귀 호출`); + return this.extractActualData(parsed); + } catch (error) { + console.log(`⚠️ JSON 파싱 실패, 원본 문자열 반환:`, error); + return [extractedData]; + } + } + + // 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출 + if (typeof extractedData === 'object' && !Array.isArray(extractedData)) { + console.log(`🔄 중첩된 객체 감지, 재귀 추출 시도`); + return this.extractActualData(extractedData); + } + + return extractedData; + } + } + + // 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기 + const objectValues = Object.values(responseData); + const arrayValue = objectValues.find(value => Array.isArray(value)); + + if (arrayValue) { + console.log(`✅ 객체 값 중 배열 발견`); + return arrayValue; + } + + // 객체의 값들 중에서 객체를 찾아서 재귀 탐색 + for (const value of objectValues) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + console.log(`🔄 객체 값에서 재귀 탐색`); + const nestedResult = this.extractActualData(value); + if (Array.isArray(nestedResult) && nestedResult.length > 0) { + return nestedResult; + } + } + } + + // 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환 + console.log(`📦 원본 객체를 단일 항목으로 처리`); + return [responseData]; + } + /** * 인바운드 데이터 매핑 처리 */ @@ -680,29 +800,56 @@ export class ImprovedButtonActionExecutor { ): Promise { try { console.log(`📥 인바운드 데이터 매핑 처리 시작`); + console.log(`📥 원본 응답 데이터:`, responseData); const targetTable = inboundMapping.targetTable; const fieldMappings = inboundMapping.fieldMappings || []; const insertMode = inboundMapping.insertMode || 'insert'; - // 응답 데이터가 배열인 경우 각 항목 처리 - const dataArray = Array.isArray(responseData) ? responseData : [responseData]; + console.log(`📥 매핑 설정:`, { + targetTable, + fieldMappings, + insertMode + }); + + // 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원) + let actualData = this.extractActualData(responseData); + + console.log(`📥 추출된 실제 데이터:`, actualData); + + // 배열이 아닌 경우 배열로 변환 + const dataArray = Array.isArray(actualData) ? actualData : [actualData]; + + console.log(`📥 처리할 데이터 배열:`, dataArray); + + if (dataArray.length === 0) { + console.log(`⚠️ 처리할 데이터가 없습니다`); + return; + } for (const item of dataArray) { const mappedData: Record = {}; + console.log(`📥 개별 아이템 처리:`, item); + // 필드 매핑 적용 for (const mapping of fieldMappings) { const sourceValue = item[mapping.sourceField]; - if (sourceValue !== undefined) { + console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`); + + if (sourceValue !== undefined && sourceValue !== null) { mappedData[mapping.targetField] = sourceValue; } } console.log(`📋 매핑된 데이터:`, mappedData); - // 데이터 저장 - await this.saveDataToTable(targetTable, mappedData, insertMode); + // 매핑된 데이터가 비어있지 않은 경우에만 저장 + if (Object.keys(mappedData).length > 0) { + await this.saveDataToTable(targetTable, mappedData, insertMode); + } else { + console.log(`⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다`); + } } console.log(`✅ 인바운드 데이터 매핑 완료`); diff --git a/frontend/types/external-call/ExternalCallTypes.ts b/frontend/types/external-call/ExternalCallTypes.ts index 2587b6b2..e38e76dc 100644 --- a/frontend/types/external-call/ExternalCallTypes.ts +++ b/frontend/types/external-call/ExternalCallTypes.ts @@ -3,10 +3,13 @@ * 데이터 저장 기능과 완전히 분리된 독립적인 타입 구조 */ +import { DataMappingConfig } from "./DataMappingTypes"; + // 외부호출 메인 설정 타입 export interface ExternalCallConfig { callType: "rest-api"; // 향후 "webhook", "email", "ftp" 등 확장 가능 restApiSettings: RestApiSettings; + dataMappingConfig?: DataMappingConfig; // 데이터 매핑 설정 추가 metadata?: { createdAt: string; updatedAt: string;