/** * πŸ”₯ κ°œμ„ λœ λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰κΈ° * * κ³„νšμ„œμ— λ”°λ₯Έ μƒˆλ‘œμš΄ μ‹€ν–‰ ν”Œλ‘œμš°: * 1. Before 타이밍 μ œμ–΄ μ‹€ν–‰ * 2. 메인 μ•‘μ…˜ μ‹€ν–‰ (replaceκ°€ μ•„λ‹Œ 경우) * 3. After 타이밍 μ œμ–΄ μ‹€ν–‰ */ import { toast } from "sonner"; import { apiClient } from "@/lib/api/client"; import { ExtendedButtonTypeConfig, ButtonDataflowConfig } from "@/types/control-management"; import { ButtonActionType } from "@/types/unified-core"; // ===== μΈν„°νŽ˜μ΄μŠ€ μ •μ˜ ===== export interface ButtonExecutionContext { buttonId: string; screenId: string; userId: string; companyCode: string; startTime: number; formData?: Record; selectedRows?: any[]; tableData?: any[]; } export interface ExecutionResult { success: boolean; message: string; executionTime: number; data?: any; error?: string; } export interface ButtonExecutionResult { success: boolean; results: ExecutionResult[]; executionTime: number; error?: string; } interface ControlConfig { type: "relationship"; relationshipConfig: { relationshipId: string; relationshipName: string; executionTiming: "before" | "after" | "replace"; contextData?: Record; }; } interface ExecutionPlan { beforeControls: ControlConfig[]; afterControls: ControlConfig[]; hasReplaceControl: boolean; } // ===== 메인 μ‹€ν–‰κΈ° 클래슀 ===== export class ImprovedButtonActionExecutor { /** * πŸ”₯ κ°œμ„ λœ λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰ */ static async executeButtonAction( buttonConfig: ExtendedButtonTypeConfig, formData: Record, context: ButtonExecutionContext ): Promise { console.log("πŸ”₯ ImprovedButtonActionExecutor μ‹œμž‘:", { buttonConfig, formData, context, }); const executionPlan = this.createExecutionPlan(buttonConfig); const results: ExecutionResult[] = []; console.log("πŸ“‹ μƒμ„±λœ μ‹€ν–‰ κ³„νš:", { beforeControls: executionPlan.beforeControls, afterControls: executionPlan.afterControls, hasReplaceControl: executionPlan.hasReplaceControl, }); try { console.log("πŸš€ λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰ μ‹œμž‘:", { actionType: buttonConfig.actionType, hasControls: executionPlan.beforeControls.length + executionPlan.afterControls.length > 0, hasReplace: executionPlan.hasReplaceControl, }); // 1. Before 타이밍 μ œμ–΄ μ‹€ν–‰ if (executionPlan.beforeControls.length > 0) { console.log("⏰ Before μ œμ–΄ μ‹€ν–‰ μ‹œμž‘"); const beforeResults = await this.executeControls( executionPlan.beforeControls, formData, context ); results.push(...beforeResults); // Before μ œμ–΄ 쀑 μ‹€νŒ¨κ°€ 있으면 쀑단 const hasFailure = beforeResults.some(r => !r.success); if (hasFailure) { throw new Error("Before μ œμ–΄ μ‹€ν–‰ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); } } // 2. 메인 μ•‘μ…˜ μ‹€ν–‰ (replaceκ°€ μ•„λ‹Œ κ²½μš°μ—λ§Œ) if (!executionPlan.hasReplaceControl) { console.log("⚑ 메인 μ•‘μ…˜ μ‹€ν–‰:", buttonConfig.actionType); const mainResult = await this.executeMainAction( buttonConfig, formData, context ); results.push(mainResult); if (!mainResult.success) { throw new Error("메인 μ•‘μ…˜ μ‹€ν–‰ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); } } else { console.log("πŸ”„ Replace λͺ¨λ“œ: 메인 μ•‘μ…˜ κ±΄λ„ˆλœ€"); } // 3. After 타이밍 μ œμ–΄ μ‹€ν–‰ if (executionPlan.afterControls.length > 0) { console.log("⏰ After μ œμ–΄ μ‹€ν–‰ μ‹œμž‘"); const afterResults = await this.executeControls( executionPlan.afterControls, formData, context ); results.push(...afterResults); } const totalExecutionTime = Date.now() - context.startTime; console.log("βœ… λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰ μ™„λ£Œ:", `${totalExecutionTime}ms`); return { success: true, results, executionTime: totalExecutionTime, }; } catch (error) { console.error("❌ λ²„νŠΌ μ•‘μ…˜ μ‹€ν–‰ μ‹€νŒ¨:", error); // λ‘€λ°± 처리 await this.handleExecutionError(error, results, buttonConfig); return { success: false, results, executionTime: Date.now() - context.startTime, error: error.message, }; } } /** * πŸ”₯ μ‹€ν–‰ κ³„νš 생성 */ private static createExecutionPlan(buttonConfig: ExtendedButtonTypeConfig): ExecutionPlan { const plan: ExecutionPlan = { beforeControls: [], afterControls: [], hasReplaceControl: false, }; const dataflowConfig = buttonConfig.dataflowConfig; if (!dataflowConfig) { console.log("⚠️ dataflowConfigκ°€ μ—†μŠ΅λ‹ˆλ‹€"); return plan; } // enableDataflowControl 체크λ₯Ό μ œκ±°ν•˜κ³  dataflowConfig만 있으면 μ‹€ν–‰ console.log("πŸ“‹ μ‹€ν–‰ κ³„νš 생성:", { controlMode: dataflowConfig.controlMode, hasRelationshipConfig: !!dataflowConfig.relationshipConfig, enableDataflowControl: buttonConfig.enableDataflowControl, }); // 관계 기반 μ œμ–΄λ§Œ 지원 if (dataflowConfig.controlMode === "relationship" && dataflowConfig.relationshipConfig) { const control: ControlConfig = { type: "relationship", relationshipConfig: dataflowConfig.relationshipConfig, }; switch (dataflowConfig.relationshipConfig.executionTiming) { case "before": plan.beforeControls.push(control); break; case "after": plan.afterControls.push(control); break; case "replace": plan.afterControls.push(control); // ReplaceλŠ” after둜 μ²˜λ¦¬ν•˜λ˜ ν”Œλž˜κ·Έ μ„€μ • plan.hasReplaceControl = true; break; } } return plan; } /** * πŸ”₯ μ œμ–΄ μ‹€ν–‰ (관계 λ˜λŠ” μ™ΈλΆ€ν˜ΈμΆœ) */ private static async executeControls( controls: ControlConfig[], formData: Record, context: ButtonExecutionContext ): Promise { const results: ExecutionResult[] = []; for (const control of controls) { try { // 관계 μ‹€ν–‰λ§Œ 지원 const result = await this.executeRelationship( control.relationshipConfig, formData, context ); results.push(result); // μ œμ–΄ μ‹€ν–‰ μ‹€νŒ¨ μ‹œ 쀑단 if (!result.success) { throw new Error(result.message); } } catch (error) { console.error(`μ œμ–΄ μ‹€ν–‰ μ‹€νŒ¨ (${control.type}):`, error); results.push({ success: false, message: `${control.type} μ œμ–΄ μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: 0, error: error.message, }); throw error; } } return results; } /** * πŸ”₯ 관계 μ‹€ν–‰ */ private static async executeRelationship( config: { relationshipId: string; relationshipName: string; executionTiming: "before" | "after" | "replace"; contextData?: Record; }, formData: Record, context: ButtonExecutionContext ): Promise { try { console.log(`πŸ”— 관계 μ‹€ν–‰ μ‹œμž‘: ${config.relationshipName} (ID: ${config.relationshipId})`); // 1. 관계 정보 쑰회 const relationshipData = await this.getRelationshipData(config.relationshipId); if (!relationshipData) { throw new Error(`관계 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: ${config.relationshipId}`); } console.log(`πŸ“‹ 관계 데이터 λ‘œλ“œ μ™„λ£Œ:`, relationshipData); // 2. 관계 νƒ€μž…μ— λ”°λ₯Έ μ‹€ν–‰ const relationships = relationshipData.relationships; const connectionType = relationships.connectionType; let result: ExecutionResult; if (connectionType === "external_call") { // μ™ΈλΆ€ 호좜 μ‹€ν–‰ result = await this.executeExternalCall(relationships, formData, context); } else if (connectionType === "data_save") { // 데이터 μ €μž₯ μ‹€ν–‰ result = await this.executeDataSave(relationships, formData, context); } else { throw new Error(`μ§€μ›ν•˜μ§€ μ•ŠλŠ” μ—°κ²° νƒ€μž…: ${connectionType}`); } console.log(`βœ… 관계 μ‹€ν–‰ μ™„λ£Œ: ${config.relationshipName}`, result); if (result.success) { toast.success(`관계 '${config.relationshipName}' μ‹€ν–‰ μ™„λ£Œ`); } else { toast.error(`관계 '${config.relationshipName}' μ‹€ν–‰ μ‹€νŒ¨: ${result.message}`); } return result; } catch (error: any) { console.error(`❌ 관계 μ‹€ν–‰ μ‹€νŒ¨: ${config.relationshipName}`, error); const errorResult = { success: false, message: `관계 '${config.relationshipName}' μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: 0, error: error.message, }; toast.error(errorResult.message); return errorResult; } } /** * 관계 데이터 쑰회 */ 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 || '관계 데이터 쑰회 μ‹€νŒ¨'); } return response.data.data; } catch (error) { console.error('관계 데이터 쑰회 였λ₯˜:', error); throw error; } } /** * μ™ΈλΆ€ 호좜 μ‹€ν–‰ */ private static async executeExternalCall( relationships: any, formData: Record, context: ButtonExecutionContext ): Promise { try { const externalCallConfig = relationships.externalCallConfig; if (!externalCallConfig) { throw new Error('μ™ΈλΆ€ 호좜 섀정이 μ—†μŠ΅λ‹ˆλ‹€'); } const restApiSettings = externalCallConfig.restApiSettings; if (!restApiSettings) { throw new Error('REST API 섀정이 μ—†μŠ΅λ‹ˆλ‹€'); } console.log(`🌐 μ™ΈλΆ€ API 호좜: ${restApiSettings.apiUrl}`); // API 호좜 μ€€λΉ„ const headers: Record = { 'Content-Type': 'application/json', ...restApiSettings.headers, }; // 인증 처리 if (restApiSettings.authentication?.type === 'api-key') { headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`; } // μš”μ²­ λ°”λ”” μ€€λΉ„ (ν…œν”Œλ¦Ώ 처리) let requestBody = restApiSettings.bodyTemplate || ''; if (requestBody) { // κ°„λ‹¨ν•œ ν…œν”Œλ¦Ώ μΉ˜ν™˜ ({{λ³€μˆ˜λͺ…}} ν˜•νƒœ) requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => { return formData[key] || (context as any).contextData?.[key] || new Date().toISOString(); }); } // λ°±μ—”λ“œ ν”„λ‘μ‹œλ₯Ό ν†΅ν•œ μ™ΈλΆ€ API 호좜 (CORS 문제 ν•΄κ²°) console.log(`🌐 λ°±μ—”λ“œ ν”„λ‘μ‹œλ₯Ό ν†΅ν•œ μ™ΈλΆ€ API 호좜 μ€€λΉ„:`, { originalUrl: restApiSettings.apiUrl, method: restApiSettings.httpMethod || 'GET', headers, body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined, }); // λ°±μ—”λ“œ ν”„λ‘μ‹œ API 호좜 - GenericApiSettings ν˜•μ‹μ— 맞게 전달 const requestPayload = { diagramId: relationships.diagramId || 45, // 관계 ID μ‚¬μš© relationshipId: relationships.relationshipId || "relationship-45", settings: { callType: "rest-api", apiType: "generic", url: restApiSettings.apiUrl, method: restApiSettings.httpMethod || 'POST', headers: restApiSettings.headers || {}, body: requestBody, authentication: restApiSettings.authentication || { type: 'none' }, timeout: restApiSettings.timeout || 30000, retryCount: restApiSettings.retryCount || 3, }, templateData: restApiSettings.httpMethod !== 'GET' ? JSON.parse(requestBody) : {}, }; console.log(`πŸ“€ λ°±μ—”λ“œλ‘œ 전솑할 데이터:`, requestPayload); const proxyResponse = await apiClient.post(`/external-calls/execute`, requestPayload); console.log(`πŸ“‘ λ°±μ—”λ“œ ν”„λ‘μ‹œ 응닡:`, proxyResponse.data); if (!proxyResponse.data.success) { throw new Error(`ν”„λ‘μ‹œ API 호좜 μ‹€νŒ¨: ${proxyResponse.data.error || proxyResponse.data.message}`); } const responseData = proxyResponse.data.result; console.log(`βœ… μ™ΈλΆ€ API 호좜 성곡 (ν”„λ‘μ‹œ):`, responseData); // 데이터 λ§€ν•‘ 처리 (inbound mapping) if (externalCallConfig.dataMappingConfig?.inboundMapping) { await this.processInboundMapping( externalCallConfig.dataMappingConfig.inboundMapping, responseData, context ); } return { success: true, message: 'μ™ΈλΆ€ 호좜 μ‹€ν–‰ μ™„λ£Œ', executionTime: Date.now() - context.startTime, data: responseData, }; } catch (error: any) { console.error('μ™ΈλΆ€ 호좜 μ‹€ν–‰ 였λ₯˜:', error); return { success: false, message: `μ™ΈλΆ€ 호좜 μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: Date.now() - context.startTime, error: error.message, }; } } /** * 데이터 μ €μž₯ μ‹€ν–‰ */ private static async executeDataSave( relationships: any, formData: Record, context: ButtonExecutionContext ): Promise { try { console.log(`πŸ’Ύ 데이터 μ €μž₯ μ‹€ν–‰ μ‹œμž‘`); // μ œμ–΄ 쑰건 확인 const controlConditions = relationships.controlConditions || []; if (controlConditions.length > 0) { const conditionsMet = this.evaluateConditions(controlConditions, formData, context); if (!conditionsMet) { return { success: false, message: 'μ œμ–΄ 쑰건을 λ§Œμ‘±ν•˜μ§€ μ•Šμ•„ 데이터 μ €μž₯을 κ±΄λ„ˆλœλ‹ˆλ‹€', executionTime: Date.now() - context.startTime, }; } } // μ•‘μ…˜ κ·Έλ£Ή μ‹€ν–‰ const actionGroups = relationships.actionGroups || []; const results = []; for (const actionGroup of actionGroups) { if (!actionGroup.isEnabled) { console.log(`⏭️ λΉ„ν™œμ„±ν™”λœ μ•‘μ…˜ κ·Έλ£Ή κ±΄λ„ˆλœ€: ${actionGroup.name}`); continue; } console.log(`🎯 μ•‘μ…˜ κ·Έλ£Ή μ‹€ν–‰: ${actionGroup.name}`); for (const action of actionGroup.actions) { if (!action.isEnabled) { console.log(`⏭️ λΉ„ν™œμ„±ν™”λœ μ•‘μ…˜ κ±΄λ„ˆλœ€: ${action.name}`); continue; } const actionResult = await this.executeDataAction( action, relationships, formData, context ); results.push(actionResult); if (!actionResult.success) { console.error(`❌ μ•‘μ…˜ μ‹€ν–‰ μ‹€νŒ¨: ${action.name}`, actionResult); } } } const successCount = results.filter(r => r.success).length; const totalCount = results.length; return { success: successCount > 0, message: `데이터 μ €μž₯ μ™„λ£Œ: ${successCount}/${totalCount} μ•‘μ…˜ 성곡`, executionTime: Date.now() - context.startTime, data: { results, successCount, totalCount, }, }; } catch (error: any) { console.error('데이터 μ €μž₯ μ‹€ν–‰ 였λ₯˜:', error); return { success: false, message: `데이터 μ €μž₯ μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: Date.now() - context.startTime, error: error.message, }; } } /** * κ°œλ³„ 데이터 μ•‘μ…˜ μ‹€ν–‰ */ private static async executeDataAction( action: any, relationships: any, formData: Record, context: ButtonExecutionContext ): Promise { try { console.log(`πŸ”§ 데이터 μ•‘μ…˜ μ‹€ν–‰: ${action.name} (${action.actionType})`); // ν•„λ“œ λ§€ν•‘ 처리 const mappedData: Record = {}; for (const mapping of action.fieldMappings) { if (mapping.valueType === 'static') { // 정적 κ°’ 처리 let value = mapping.value; if (value === '#NOW') { value = new Date().toISOString(); } mappedData[mapping.targetField] = value; } else { // ν•„λ“œ λ§€ν•‘ 처리 const sourceField = mapping.fromField?.columnName; if (sourceField && formData[sourceField] !== undefined) { mappedData[mapping.toField.columnName] = formData[sourceField]; } } } console.log(`πŸ“‹ λ§€ν•‘λœ 데이터:`, mappedData); // λŒ€μƒ μ—°κ²° 정보 const toConnection = relationships.toConnection; const targetTable = relationships.toTable?.tableName; if (!targetTable) { throw new Error('λŒ€μƒ ν…Œμ΄λΈ”μ΄ μ§€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€'); } // 데이터 μ €μž₯ API 호좜 const saveResult = await this.saveDataToTable( targetTable, mappedData, action.actionType, toConnection ); return { success: true, message: `데이터 μ•‘μ…˜ "${action.name}" μ‹€ν–‰ μ™„λ£Œ`, executionTime: Date.now() - context.startTime, data: saveResult, }; } catch (error: any) { console.error(`데이터 μ•‘μ…˜ μ‹€ν–‰ 였λ₯˜: ${action.name}`, error); return { success: false, message: `데이터 μ•‘μ…˜ μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: Date.now() - context.startTime, error: error.message, }; } } /** * ν…Œμ΄λΈ”μ— 데이터 μ €μž₯ */ private static async saveDataToTable( tableName: string, data: Record, actionType: string, 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, }), }); if (!response.ok) { throw new Error(`데이터 μ €μž₯ API 호좜 μ‹€νŒ¨: ${response.status}`); } return await response.json(); } catch (error) { console.error('데이터 μ €μž₯ 였λ₯˜:', error); throw error; } } /** * 쑰건 평가 */ private static evaluateConditions( conditions: any[], formData: Record, context: ButtonExecutionContext ): boolean { for (const condition of conditions) { const fieldValue = formData[condition.field]; const conditionValue = condition.value; const operator = condition.operator; let conditionMet = false; switch (operator) { case '=': conditionMet = fieldValue === conditionValue; break; case '!=': conditionMet = fieldValue !== conditionValue; break; case '>': conditionMet = Number(fieldValue) > Number(conditionValue); break; case '<': conditionMet = Number(fieldValue) < Number(conditionValue); break; case '>=': conditionMet = Number(fieldValue) >= Number(conditionValue); break; case '<=': conditionMet = Number(fieldValue) <= Number(conditionValue); break; default: console.warn(`μ§€μ›ν•˜μ§€ μ•ŠλŠ” μ—°μ‚°μž: ${operator}`); conditionMet = true; } if (!conditionMet) { console.log(`❌ 쑰건 뢈만쑱: ${condition.field} ${operator} ${conditionValue} (μ‹€μ œκ°’: ${fieldValue})`); return false; } } console.log(`βœ… λͺ¨λ“  쑰건 만쑱`); return true; } /** * μΈλ°”μš΄λ“œ 데이터 λ§€ν•‘ 처리 */ private static async processInboundMapping( inboundMapping: any, responseData: any, context: ButtonExecutionContext ): Promise { try { console.log(`πŸ“₯ μΈλ°”μš΄λ“œ 데이터 λ§€ν•‘ 처리 μ‹œμž‘`); const targetTable = inboundMapping.targetTable; const fieldMappings = inboundMapping.fieldMappings || []; const insertMode = inboundMapping.insertMode || 'insert'; // 응닡 데이터가 배열인 경우 각 ν•­λͺ© 처리 const dataArray = Array.isArray(responseData) ? responseData : [responseData]; for (const item of dataArray) { const mappedData: Record = {}; // ν•„λ“œ λ§€ν•‘ 적용 for (const mapping of fieldMappings) { const sourceValue = item[mapping.sourceField]; if (sourceValue !== undefined) { mappedData[mapping.targetField] = sourceValue; } } console.log(`πŸ“‹ λ§€ν•‘λœ 데이터:`, mappedData); // 데이터 μ €μž₯ await this.saveDataToTable(targetTable, mappedData, insertMode); } console.log(`βœ… μΈλ°”μš΄λ“œ 데이터 λ§€ν•‘ μ™„λ£Œ`); } catch (error) { console.error('μΈλ°”μš΄λ“œ 데이터 λ§€ν•‘ 였λ₯˜:', error); throw error; } } /** * πŸ”₯ 메인 μ•‘μ…˜ μ‹€ν–‰ */ private static async executeMainAction( buttonConfig: ExtendedButtonTypeConfig, formData: Record, context: ButtonExecutionContext ): Promise { try { // κΈ°μ‘΄ ButtonActionExecutor λ‘œμ§μ„ μ—¬κΈ°μ„œ ν˜ΈμΆœν•˜κ±°λ‚˜ // κ°„λ‹¨ν•œ μ•‘μ…˜λ“€μ„ 직접 κ΅¬ν˜„ const startTime = performance.now(); // μž„μ‹œ κ΅¬ν˜„ - μ‹€μ œλ‘œλŠ” κΈ°μ‘΄ ButtonActionExecutorλ₯Ό ν˜ΈμΆœν•΄μ•Ό 함 const result = { success: true, message: `${buttonConfig.actionType} μ•‘μ…˜ μ‹€ν–‰ μ™„λ£Œ`, executionTime: performance.now() - startTime, data: { actionType: buttonConfig.actionType, formData }, }; console.log("βœ… 메인 μ•‘μ…˜ μ‹€ν–‰ μ™„λ£Œ:", result.message); return result; } catch (error) { console.error("메인 μ•‘μ…˜ μ‹€ν–‰ 였λ₯˜:", error); return { success: false, message: `${buttonConfig.actionType} μ•‘μ…˜ μ‹€ν–‰ μ‹€νŒ¨: ${error.message}`, executionTime: 0, error: error.message, }; } } /** * πŸ”₯ μ‹€ν–‰ 였λ₯˜ 처리 및 λ‘€λ°± */ private static async handleExecutionError( error: Error, results: ExecutionResult[], buttonConfig: ExtendedButtonTypeConfig ): Promise { console.error("πŸ”„ μ‹€ν–‰ 였λ₯˜ 처리 μ‹œμž‘:", error.message); // 둀백이 ν•„μš”ν•œ 경우 처리 const rollbackNeeded = buttonConfig.dataflowConfig?.executionOptions?.rollbackOnError; if (rollbackNeeded) { console.log("πŸ”„ λ‘€λ°± 처리 μ‹œμž‘..."); // μ„±κ³΅ν•œ 결과듀을 μ—­μˆœμœΌλ‘œ λ‘€λ°± const successfulResults = results.filter(r => r.success).reverse(); for (const result of successfulResults) { try { // λ‘€λ°± 둜직 κ΅¬ν˜„ (ν•„μš”μ‹œ) console.log("πŸ”„ λ‘€λ°±:", result.message); } catch (rollbackError) { console.error("λ‘€λ°± μ‹€νŒ¨:", rollbackError); } } } // 였λ₯˜ ν† μŠ€νŠΈ ν‘œμ‹œ toast.error(error.message || "μž‘μ—… 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); } }