From d22e83d23453b158155aa46fb350fbe1c3890b7c Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 15:51:13 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20UPDATE=20=EC=95=A1=EC=85=98=20formData?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=ED=8F=AC=ED=95=A8=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UPDATE 액션 실행 시: - formData를 기본으로 복사하여 기본키 포함 - 상세 로깅으로 디버깅 지원 - 백엔드 동적 기본키 조회 구현 --- .../dataflowExecutionController.ts | 106 +++++--- .../lib/utils/improvedButtonActionExecutor.ts | 232 ++++++++---------- 2 files changed, 174 insertions(+), 164 deletions(-) diff --git a/backend-node/src/controllers/dataflowExecutionController.ts b/backend-node/src/controllers/dataflowExecutionController.ts index 71514633..338fa628 100644 --- a/backend-node/src/controllers/dataflowExecutionController.ts +++ b/backend-node/src/controllers/dataflowExecutionController.ts @@ -29,13 +29,23 @@ export async function executeDataAction( // 연결 정보에 따라 다른 데이터베이스에 저장 let result; - + if (connection && connection.id !== 0) { // 외부 데이터베이스 연결 - result = await executeExternalDatabaseAction(tableName, data, actionType, connection); + result = await executeExternalDatabaseAction( + tableName, + data, + actionType, + connection + ); } else { // 메인 데이터베이스 (현재 시스템) - result = await executeMainDatabaseAction(tableName, data, actionType, companyCode); + result = await executeMainDatabaseAction( + tableName, + data, + actionType, + companyCode + ); } logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result); @@ -45,7 +55,6 @@ export async function executeDataAction( message: `데이터 액션 실행 완료: ${actionType}`, data: result, }); - } catch (error: any) { logger.error("데이터 액션 실행 실패:", error); res.status(500).json({ @@ -73,13 +82,13 @@ async function executeMainDatabaseAction( }; switch (actionType.toLowerCase()) { - case 'insert': + case "insert": return await executeInsert(tableName, dataWithCompany); - case 'update': + case "update": return await executeUpdate(tableName, dataWithCompany); - case 'upsert': + case "upsert": return await executeUpsert(tableName, dataWithCompany); - case 'delete': + case "delete": return await executeDelete(tableName, dataWithCompany); default: throw new Error(`지원하지 않는 액션 타입: ${actionType}`); @@ -100,25 +109,37 @@ async function executeExternalDatabaseAction( connection: any ): Promise { try { - logger.info(`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`); + logger.info( + `외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})` + ); logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data); // 🔥 실제 외부 DB 연결 및 실행 로직 구현 - const { MultiConnectionQueryService } = await import('../services/multiConnectionQueryService'); + const { MultiConnectionQueryService } = await import( + "../services/multiConnectionQueryService" + ); const queryService = new MultiConnectionQueryService(); let result; switch (actionType.toLowerCase()) { - case 'insert': - result = await queryService.insertDataToConnection(connection.id, tableName, data); + case "insert": + result = await queryService.insertDataToConnection( + connection.id, + tableName, + data + ); logger.info(`외부 DB INSERT 성공:`, result); break; - case 'update': + case "update": // TODO: UPDATE 로직 구현 (조건 필요) - throw new Error('UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.'); - case 'delete': + throw new Error( + "UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다." + ); + case "delete": // TODO: DELETE 로직 구현 (조건 필요) - throw new Error('DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다.'); + throw new Error( + "DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다." + ); default: throw new Error(`지원하지 않는 액션 타입: ${actionType}`); } @@ -139,12 +160,15 @@ async function executeExternalDatabaseAction( /** * INSERT 실행 */ -async function executeInsert(tableName: string, data: Record): Promise { +async function executeInsert( + tableName: string, + data: Record +): Promise { try { // 동적 테이블 접근을 위한 raw query 사용 - const columns = Object.keys(data).join(', '); + const columns = Object.keys(data).join(", "); const values = Object.values(data); - const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); + const placeholders = values.map((_, index) => `$${index + 1}`).join(", "); const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`; @@ -154,7 +178,7 @@ async function executeInsert(tableName: string, data: Record): Prom return { success: true, - action: 'insert', + action: "insert", tableName, data: result, affectedRows: result.length, @@ -168,7 +192,10 @@ async function executeInsert(tableName: string, data: Record): Prom /** * UPDATE 실행 */ -async function executeUpdate(tableName: string, data: Record): Promise { +async function executeUpdate( + tableName: string, + data: Record +): Promise { try { logger.info(`UPDATE 액션 시작:`, { tableName, receivedData: data }); @@ -179,9 +206,11 @@ async function executeUpdate(tableName: string, data: Record): Prom JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary `; - - const pkResult = await query<{ column_name: string }>(primaryKeyQuery, [tableName]); - + + const pkResult = await query<{ column_name: string }>(primaryKeyQuery, [ + tableName, + ]); + if (!pkResult || pkResult.length === 0) { throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다`); } @@ -191,14 +220,16 @@ async function executeUpdate(tableName: string, data: Record): Prom // 2. 기본키 값 추출 const primaryKeyValue = data[primaryKeyColumn]; - + if (!primaryKeyValue && primaryKeyValue !== 0) { logger.error(`UPDATE 실패: 기본키 값이 없음`, { primaryKeyColumn, receivedData: data, availableKeys: Object.keys(data), }); - throw new Error(`UPDATE를 위한 기본키 값이 필요합니다 (${primaryKeyColumn})`); + throw new Error( + `UPDATE를 위한 기본키 값이 필요합니다 (${primaryKeyColumn})` + ); } // 3. 업데이트할 데이터에서 기본키 제외 @@ -214,12 +245,15 @@ async function executeUpdate(tableName: string, data: Record): Prom // 4. 동적 UPDATE 쿼리 생성 const setClause = Object.keys(updateData) .map((key, index) => `${key} = $${index + 1}`) - .join(', '); + .join(", "); const values = Object.values(updateData); const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE ${primaryKeyColumn} = $${values.length + 1} RETURNING *`; - logger.info(`UPDATE 쿼리 실행:`, { query: updateQuery, values: [...values, primaryKeyValue] }); + logger.info(`UPDATE 쿼리 실행:`, { + query: updateQuery, + values: [...values, primaryKeyValue], + }); const result = await query(updateQuery, [...values, primaryKeyValue]); @@ -227,7 +261,7 @@ async function executeUpdate(tableName: string, data: Record): Prom return { success: true, - action: 'update', + action: "update", tableName, data: result, affectedRows: result.length, @@ -241,7 +275,10 @@ async function executeUpdate(tableName: string, data: Record): Prom /** * UPSERT 실행 */ -async function executeUpsert(tableName: string, data: Record): Promise { +async function executeUpsert( + tableName: string, + data: Record +): Promise { try { // 먼저 INSERT를 시도하고, 실패하면 UPDATE try { @@ -260,12 +297,15 @@ async function executeUpsert(tableName: string, data: Record): Prom /** * DELETE 실행 */ -async function executeDelete(tableName: string, data: Record): Promise { +async function executeDelete( + tableName: string, + data: Record +): Promise { try { const { id } = data; if (!id) { - throw new Error('DELETE를 위한 ID가 필요합니다'); + throw new Error("DELETE를 위한 ID가 필요합니다"); } const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`; @@ -276,7 +316,7 @@ async function executeDelete(tableName: string, data: Record): Prom return { success: true, - action: 'delete', + action: "delete", tableName, data: result, affectedRows: result.length, diff --git a/frontend/lib/utils/improvedButtonActionExecutor.ts b/frontend/lib/utils/improvedButtonActionExecutor.ts index fed8a636..9b4388f4 100644 --- a/frontend/lib/utils/improvedButtonActionExecutor.ts +++ b/frontend/lib/utils/improvedButtonActionExecutor.ts @@ -1,6 +1,6 @@ /** * 🔥 개선된 버튼 액션 실행기 - * + * * 계획서에 따른 새로운 실행 플로우: * 1. Before 타이밍 제어 실행 * 2. 메인 액션 실행 (replace가 아닌 경우) @@ -65,7 +65,7 @@ export class ImprovedButtonActionExecutor { static async executeButtonAction( buttonConfig: ExtendedButtonTypeConfig, formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { console.log("🔥 ImprovedButtonActionExecutor 시작:", { buttonConfig, @@ -92,15 +92,11 @@ export class ImprovedButtonActionExecutor { // 1. Before 타이밍 제어 실행 if (executionPlan.beforeControls.length > 0) { console.log("⏰ Before 제어 실행 시작"); - const beforeResults = await this.executeControls( - executionPlan.beforeControls, - formData, - context - ); + const beforeResults = await this.executeControls(executionPlan.beforeControls, formData, context); results.push(...beforeResults); // Before 제어 중 실패가 있으면 중단 - const hasFailure = beforeResults.some(r => !r.success); + const hasFailure = beforeResults.some((r) => !r.success); if (hasFailure) { throw new Error("Before 제어 실행 중 오류가 발생했습니다."); } @@ -109,11 +105,7 @@ export class ImprovedButtonActionExecutor { // 2. 메인 액션 실행 (replace가 아닌 경우에만) if (!executionPlan.hasReplaceControl) { console.log("⚡ 메인 액션 실행:", buttonConfig.actionType); - const mainResult = await this.executeMainAction( - buttonConfig, - formData, - context - ); + const mainResult = await this.executeMainAction(buttonConfig, formData, context); results.push(mainResult); if (!mainResult.success) { @@ -126,11 +118,7 @@ export class ImprovedButtonActionExecutor { // 3. After 타이밍 제어 실행 if (executionPlan.afterControls.length > 0) { console.log("⏰ After 제어 실행 시작"); - const afterResults = await this.executeControls( - executionPlan.afterControls, - formData, - context - ); + const afterResults = await this.executeControls(executionPlan.afterControls, formData, context); results.push(...afterResults); } @@ -144,10 +132,10 @@ export class ImprovedButtonActionExecutor { }; } catch (error) { console.error("❌ 버튼 액션 실행 실패:", error); - + // 롤백 처리 await this.handleExecutionError(error, results, buttonConfig); - + return { success: false, results, @@ -210,18 +198,14 @@ export class ImprovedButtonActionExecutor { private static async executeControls( controls: ControlConfig[], formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { const results: ExecutionResult[] = []; for (const control of controls) { try { // 관계 실행만 지원 - const result = await this.executeRelationship( - control.relationshipConfig, - formData, - context - ); + const result = await this.executeRelationship(control.relationshipConfig, formData, context); results.push(result); @@ -255,7 +239,7 @@ export class ImprovedButtonActionExecutor { contextData?: Record; }, formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { try { console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`); @@ -271,7 +255,7 @@ export class ImprovedButtonActionExecutor { // 2. 관계 타입에 따른 실행 const relationships = relationshipData.relationships; const connectionType = relationships.connectionType; - + console.log(`🔍 관계 상세 정보:`, { connectionType, hasExternalCallConfig: !!relationships.externalCallConfig, @@ -301,7 +285,6 @@ export class ImprovedButtonActionExecutor { } return result; - } catch (error: any) { console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error); const errorResult = { @@ -310,7 +293,7 @@ export class ImprovedButtonActionExecutor { executionTime: 0, error: error.message, }; - + toast.error(errorResult.message); return errorResult; } @@ -322,18 +305,18 @@ export class ImprovedButtonActionExecutor { private static async getRelationshipData(relationshipId: string): Promise { try { console.log(`🔍 관계 데이터 조회 시작: ${relationshipId}`); - + const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`); - + console.log(`✅ 관계 데이터 조회 성공:`, response.data); - + if (!response.data.success) { - throw new Error(response.data.message || '관계 데이터 조회 실패'); + throw new Error(response.data.message || "관계 데이터 조회 실패"); } - + return response.data.data; } catch (error) { - console.error('관계 데이터 조회 오류:', error); + console.error("관계 데이터 조회 오류:", error); throw error; } } @@ -344,39 +327,39 @@ export class ImprovedButtonActionExecutor { private static async executeExternalCall( relationships: any, formData: Record, - context: ButtonExecutionContext + 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('외부 호출 설정이 없습니다'); + console.error("❌ 외부 호출 설정이 없습니다. relationships 구조:", relationships); + throw new Error("외부 호출 설정이 없습니다"); } const restApiSettings = externalCallConfig.restApiSettings; if (!restApiSettings) { - throw new Error('REST API 설정이 없습니다'); + throw new Error("REST API 설정이 없습니다"); } console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`); // API 호출 준비 const headers: Record = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", ...restApiSettings.headers, }; // 인증 처리 - if (restApiSettings.authentication?.type === 'api-key') { - headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`; + if (restApiSettings.authentication?.type === "api-key") { + headers["Authorization"] = `Bearer ${restApiSettings.authentication.apiKey}`; } // 요청 바디 준비 (템플릿 처리) - let requestBody = restApiSettings.bodyTemplate || ''; + let requestBody = restApiSettings.bodyTemplate || ""; if (requestBody) { // 간단한 템플릿 치환 ({{변수명}} 형태) requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => { @@ -387,9 +370,9 @@ export class ImprovedButtonActionExecutor { // 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결) console.log(`🌐 백엔드 프록시를 통한 외부 API 호출 준비:`, { originalUrl: restApiSettings.apiUrl, - method: restApiSettings.httpMethod || 'GET', + method: restApiSettings.httpMethod || "GET", headers, - body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined, + body: restApiSettings.httpMethod !== "GET" ? requestBody : undefined, }); // 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달 @@ -400,14 +383,14 @@ export class ImprovedButtonActionExecutor { callType: "rest-api", apiType: "generic", url: restApiSettings.apiUrl, - method: restApiSettings.httpMethod || 'POST', + method: restApiSettings.httpMethod || "POST", headers: restApiSettings.headers || {}, body: requestBody, - authentication: restApiSettings.authentication || { type: 'none' }, + authentication: restApiSettings.authentication || { type: "none" }, timeout: restApiSettings.timeout || 30000, retryCount: restApiSettings.retryCount || 3, }, - templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData, + templateData: restApiSettings.httpMethod !== "GET" && requestBody ? JSON.parse(requestBody) : formData, }; console.log(`📤 백엔드로 전송할 데이터:`, requestPayload); @@ -428,25 +411,20 @@ export class ImprovedButtonActionExecutor { console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`); console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping); console.log(`📥 응답 데이터:`, responseData); - - await this.processInboundMapping( - externalCallConfig.dataMappingConfig.inboundMapping, - responseData, - context - ); + + await this.processInboundMapping(externalCallConfig.dataMappingConfig.inboundMapping, responseData, context); } else { console.log(`ℹ️ 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`); } return { success: true, - message: '외부 호출 실행 완료', + message: "외부 호출 실행 완료", executionTime: Date.now() - context.startTime, data: responseData, }; - } catch (error: any) { - console.error('외부 호출 실행 오류:', error); + console.error("외부 호출 실행 오류:", error); return { success: false, message: `외부 호출 실행 실패: ${error.message}`, @@ -462,7 +440,7 @@ export class ImprovedButtonActionExecutor { private static async executeDataSave( relationships: any, formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { try { console.log(`💾 데이터 저장 실행 시작`); @@ -474,7 +452,7 @@ export class ImprovedButtonActionExecutor { if (!conditionsMet) { return { success: false, - message: '제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다', + message: "제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다", executionTime: Date.now() - context.startTime, }; } @@ -498,12 +476,7 @@ export class ImprovedButtonActionExecutor { continue; } - const actionResult = await this.executeDataAction( - action, - relationships, - formData, - context - ); + const actionResult = await this.executeDataAction(action, relationships, formData, context); results.push(actionResult); if (!actionResult.success) { @@ -512,7 +485,7 @@ export class ImprovedButtonActionExecutor { } } - const successCount = results.filter(r => r.success).length; + const successCount = results.filter((r) => r.success).length; const totalCount = results.length; return { @@ -525,9 +498,8 @@ export class ImprovedButtonActionExecutor { totalCount, }, }; - } catch (error: any) { - console.error('데이터 저장 실행 오류:', error); + console.error("데이터 저장 실행 오류:", error); return { success: false, message: `데이터 저장 실행 실패: ${error.message}`, @@ -544,19 +516,20 @@ export class ImprovedButtonActionExecutor { action: any, relationships: any, formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { try { console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`); + console.log(`📥 받은 formData:`, formData); + + // 🔥 UPDATE 액션의 경우 formData를 기본으로 시작 + const mappedData: Record = action.actionType === "update" ? { ...formData } : {}; - // 필드 매핑 처리 - const mappedData: Record = {}; - for (const mapping of action.fieldMappings) { - if (mapping.valueType === 'static') { + if (mapping.valueType === "static") { // 정적 값 처리 let value = mapping.value; - if (value === '#NOW') { + if (value === "#NOW") { value = new Date().toISOString(); } mappedData[mapping.targetField] = value; @@ -570,22 +543,21 @@ export class ImprovedButtonActionExecutor { } console.log(`📋 매핑된 데이터:`, mappedData); + console.log(`🔑 기본키 포함 여부 체크:`, { + hasId: "id" in mappedData, + keys: Object.keys(mappedData), + }); // 대상 연결 정보 const toConnection = relationships.toConnection; const targetTable = relationships.toTable?.tableName; if (!targetTable) { - throw new Error('대상 테이블이 지정되지 않았습니다'); + throw new Error("대상 테이블이 지정되지 않았습니다"); } // 데이터 저장 API 호출 - const saveResult = await this.saveDataToTable( - targetTable, - mappedData, - action.actionType, - toConnection - ); + const saveResult = await this.saveDataToTable(targetTable, mappedData, action.actionType, toConnection); return { success: true, @@ -593,7 +565,6 @@ export class ImprovedButtonActionExecutor { executionTime: Date.now() - context.startTime, data: saveResult, }; - } catch (error: any) { console.error(`데이터 액션 실행 오류: ${action.name}`, error); return { @@ -612,17 +583,17 @@ export class ImprovedButtonActionExecutor { tableName: string, data: Record, actionType: string, - connection?: any + connection?: any, ): Promise { try { console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, { actionType, data, - connection + connection, }); // 데이터 저장 API 호출 (apiClient 사용) - const response = await apiClient.post('/dataflow/execute-data-action', { + const response = await apiClient.post("/dataflow/execute-data-action", { tableName, data, actionType, @@ -632,7 +603,7 @@ export class ImprovedButtonActionExecutor { console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data); return response.data; } catch (error) { - console.error('테이블 데이터 저장 오류:', error); + console.error("테이블 데이터 저장 오류:", error); throw error; } } @@ -643,7 +614,7 @@ export class ImprovedButtonActionExecutor { private static evaluateConditions( conditions: any[], formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): boolean { console.log(`🔍 조건 평가 시작:`, { conditions, @@ -668,22 +639,22 @@ export class ImprovedButtonActionExecutor { let conditionMet = false; switch (operator) { - case '=': + case "=": conditionMet = fieldValue === conditionValue; break; - case '!=': + case "!=": conditionMet = fieldValue !== conditionValue; break; - case '>': + case ">": conditionMet = Number(fieldValue) > Number(conditionValue); break; - case '<': + case "<": conditionMet = Number(fieldValue) < Number(conditionValue); break; - case '>=': + case ">=": conditionMet = Number(fieldValue) >= Number(conditionValue); break; - case '<=': + case "<=": conditionMet = Number(fieldValue) <= Number(conditionValue); break; default: @@ -708,7 +679,7 @@ export class ImprovedButtonActionExecutor { */ private static extractActualData(responseData: any): any { console.log(`🔍 데이터 추출 시작 - 원본 타입: ${typeof responseData}`); - + // null이나 undefined인 경우 if (!responseData) { console.log(`⚠️ 응답 데이터가 null 또는 undefined`); @@ -722,7 +693,7 @@ export class ImprovedButtonActionExecutor { } // 문자열인 경우 JSON 파싱 시도 - if (typeof responseData === 'string') { + if (typeof responseData === "string") { console.log(`🔄 JSON 문자열 파싱 시도`); try { const parsed = JSON.parse(responseData); @@ -735,33 +706,33 @@ export class ImprovedButtonActionExecutor { } // 객체가 아닌 경우 (숫자 등) - if (typeof responseData !== 'object') { + 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: [...] } + "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') { + if (typeof extractedData === "string") { console.log(`🔄 추출된 데이터가 JSON 문자열, 파싱 시도`); try { const parsed = JSON.parse(extractedData); @@ -772,21 +743,21 @@ export class ImprovedButtonActionExecutor { return [extractedData]; } } - + // 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출 - if (typeof extractedData === 'object' && !Array.isArray(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)); - + const arrayValue = objectValues.find((value) => Array.isArray(value)); + if (arrayValue) { console.log(`✅ 객체 값 중 배열 발견`); return arrayValue; @@ -794,7 +765,7 @@ export class ImprovedButtonActionExecutor { // 객체의 값들 중에서 객체를 찾아서 재귀 탐색 for (const value of objectValues) { - if (value && typeof value === 'object' && !Array.isArray(value)) { + if (value && typeof value === "object" && !Array.isArray(value)) { console.log(`🔄 객체 값에서 재귀 탐색`); const nestedResult = this.extractActualData(value); if (Array.isArray(nestedResult) && nestedResult.length > 0) { @@ -814,7 +785,7 @@ export class ImprovedButtonActionExecutor { private static async processInboundMapping( inboundMapping: any, responseData: any, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { try { console.log(`📥 인바운드 데이터 매핑 처리 시작`); @@ -822,22 +793,22 @@ export class ImprovedButtonActionExecutor { const targetTable = inboundMapping.targetTable; const fieldMappings = inboundMapping.fieldMappings || []; - const insertMode = inboundMapping.insertMode || 'insert'; + const insertMode = inboundMapping.insertMode || "insert"; console.log(`📥 매핑 설정:`, { targetTable, fieldMappings, - insertMode + insertMode, }); // 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원) let actualData = this.extractActualData(responseData); - + console.log(`📥 추출된 실제 데이터:`, actualData); // 배열이 아닌 경우 배열로 변환 const dataArray = Array.isArray(actualData) ? actualData : [actualData]; - + console.log(`📥 처리할 데이터 배열:`, dataArray); if (dataArray.length === 0) { @@ -854,7 +825,7 @@ export class ImprovedButtonActionExecutor { for (const mapping of fieldMappings) { const sourceValue = item[mapping.sourceField]; console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`); - + if (sourceValue !== undefined && sourceValue !== null) { mappedData[mapping.targetField] = sourceValue; } @@ -872,19 +843,18 @@ export class ImprovedButtonActionExecutor { console.log(`✅ 인바운드 데이터 매핑 완료`); } catch (error) { - console.error('인바운드 데이터 매핑 오류:', error); + console.error("인바운드 데이터 매핑 오류:", error); throw error; } } - /** * 🔥 메인 액션 실행 */ private static async executeMainAction( buttonConfig: ExtendedButtonTypeConfig, formData: Record, - context: ButtonExecutionContext + context: ButtonExecutionContext, ): Promise { try { // 기존 ButtonActionExecutor 로직을 여기서 호출하거나 @@ -918,7 +888,7 @@ export class ImprovedButtonActionExecutor { private static async handleExecutionError( error: Error, results: ExecutionResult[], - buttonConfig: ExtendedButtonTypeConfig + buttonConfig: ExtendedButtonTypeConfig, ): Promise { console.error("🔄 실행 오류 처리 시작:", error.message); @@ -926,10 +896,10 @@ export class ImprovedButtonActionExecutor { const rollbackNeeded = buttonConfig.dataflowConfig?.executionOptions?.rollbackOnError; if (rollbackNeeded) { console.log("🔄 롤백 처리 시작..."); - + // 성공한 결과들을 역순으로 롤백 - const successfulResults = results.filter(r => r.success).reverse(); - + const successfulResults = results.filter((r) => r.success).reverse(); + for (const result of successfulResults) { try { // 롤백 로직 구현 (필요시)