import { query, queryOne } from "../database/db"; import { logger } from "../utils/logger"; // 외부 호출 설정 타입 정의 export interface ExternalCallConfig { id?: number; config_name: string; call_type: string; api_type?: string; config_data: any; description?: string; company_code?: string; is_active?: string; created_by?: string; updated_by?: string; } export interface ExternalCallConfigFilter { company_code?: string; call_type?: string; api_type?: string; is_active?: string; search?: string; } export class ExternalCallConfigService { /** * 외부 호출 설정 목록 조회 */ async getConfigs( filter: ExternalCallConfigFilter = {} ): Promise { try { logger.info("=== 외부 호출 설정 목록 조회 시작 ==="); logger.info(`필터 조건:`, filter); const conditions: string[] = []; const params: any[] = []; let paramIndex = 1; // 회사 코드 필터 if (filter.company_code) { conditions.push(`company_code = $${paramIndex++}`); params.push(filter.company_code); } // 호출 타입 필터 if (filter.call_type) { conditions.push(`call_type = $${paramIndex++}`); params.push(filter.call_type); } // API 타입 필터 if (filter.api_type) { conditions.push(`api_type = $${paramIndex++}`); params.push(filter.api_type); } // 활성화 상태 필터 if (filter.is_active) { conditions.push(`is_active = $${paramIndex++}`); params.push(filter.is_active); } // 검색어 필터 (설정 이름 또는 설명) if (filter.search) { conditions.push( `(config_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})` ); params.push(`%${filter.search}%`); paramIndex++; } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const configs = await query( `SELECT * FROM external_call_configs ${whereClause} ORDER BY is_active DESC, created_date DESC`, params ); logger.info(`외부 호출 설정 조회 결과: ${configs.length}개`); return configs; } catch (error) { logger.error("외부 호출 설정 목록 조회 실패:", error); throw error; } } /** * 외부 호출 설정 단일 조회 */ async getConfigById(id: number): Promise { try { logger.info(`=== 외부 호출 설정 조회: ID ${id} ===`); const config = await queryOne( `SELECT * FROM external_call_configs WHERE id = $1`, [id] ); if (config) { logger.info(`외부 호출 설정 조회 성공: ${config.config_name}`); } else { logger.warn(`외부 호출 설정을 찾을 수 없음: ID ${id}`); } return config || null; } catch (error) { logger.error(`외부 호출 설정 조회 실패 (ID: ${id}):`, error); throw error; } } /** * 외부 호출 설정 생성 */ async createConfig(data: ExternalCallConfig): Promise { try { logger.info("=== 외부 호출 설정 생성 시작 ==="); logger.info(`생성할 설정:`, { config_name: data.config_name, call_type: data.call_type, api_type: data.api_type, company_code: data.company_code || "*", }); // 중복 이름 검사 const existingConfig = await queryOne( `SELECT * FROM external_call_configs WHERE config_name = $1 AND company_code = $2 AND is_active = $3`, [data.config_name, data.company_code || "*", "Y"] ); if (existingConfig) { throw new Error( `동일한 이름의 외부 호출 설정이 이미 존재합니다: ${data.config_name}` ); } const newConfig = await queryOne( `INSERT INTO external_call_configs (config_name, call_type, api_type, config_data, description, company_code, is_active, created_by, updated_by, created_date, updated_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) RETURNING *`, [ data.config_name, data.call_type, data.api_type, JSON.stringify(data.config_data), data.description, data.company_code || "*", data.is_active || "Y", data.created_by, data.updated_by, ] ); logger.info( `외부 호출 설정 생성 완료: ${newConfig!.config_name} (ID: ${newConfig!.id})` ); return newConfig!; } catch (error) { logger.error("외부 호출 설정 생성 실패:", error); throw error; } } /** * 외부 호출 설정 수정 */ async updateConfig( id: number, data: Partial ): Promise { try { logger.info(`=== 외부 호출 설정 수정 시작: ID ${id} ===`); // 기존 설정 존재 확인 const existingConfig = await this.getConfigById(id); if (!existingConfig) { throw new Error(`외부 호출 설정을 찾을 수 없습니다: ID ${id}`); } // 이름 중복 검사 (다른 설정과 중복되는지) if (data.config_name && data.config_name !== existingConfig.config_name) { const duplicateConfig = await queryOne( `SELECT * FROM external_call_configs WHERE config_name = $1 AND company_code = $2 AND is_active = $3 AND id != $4`, [ data.config_name, data.company_code || existingConfig.company_code, "Y", id, ] ); if (duplicateConfig) { throw new Error( `동일한 이름의 외부 호출 설정이 이미 존재합니다: ${data.config_name}` ); } } // 동적 UPDATE 쿼리 생성 const updateFields: string[] = ["updated_date = NOW()"]; const params: any[] = []; let paramIndex = 1; if (data.config_name) { updateFields.push(`config_name = $${paramIndex++}`); params.push(data.config_name); } if (data.call_type) { updateFields.push(`call_type = $${paramIndex++}`); params.push(data.call_type); } if (data.api_type !== undefined) { updateFields.push(`api_type = $${paramIndex++}`); params.push(data.api_type); } if (data.config_data) { updateFields.push(`config_data = $${paramIndex++}`); params.push(JSON.stringify(data.config_data)); } if (data.description !== undefined) { updateFields.push(`description = $${paramIndex++}`); params.push(data.description); } if (data.company_code) { updateFields.push(`company_code = $${paramIndex++}`); params.push(data.company_code); } if (data.is_active) { updateFields.push(`is_active = $${paramIndex++}`); params.push(data.is_active); } if (data.updated_by) { updateFields.push(`updated_by = $${paramIndex++}`); params.push(data.updated_by); } params.push(id); const updatedConfig = await queryOne( `UPDATE external_call_configs SET ${updateFields.join(", ")} WHERE id = $${paramIndex} RETURNING *`, params ); logger.info( `외부 호출 설정 수정 완료: ${updatedConfig!.config_name} (ID: ${id})` ); return updatedConfig!; } catch (error) { logger.error(`외부 호출 설정 수정 실패 (ID: ${id}):`, error); throw error; } } /** * 외부 호출 설정 삭제 (논리 삭제) */ async deleteConfig(id: number, deletedBy?: string): Promise { try { logger.info(`=== 외부 호출 설정 삭제 시작: ID ${id} ===`); // 기존 설정 존재 확인 const existingConfig = await this.getConfigById(id); if (!existingConfig) { throw new Error(`외부 호출 설정을 찾을 수 없습니다: ID ${id}`); } // 논리 삭제 (is_active = 'N') await query( `UPDATE external_call_configs SET is_active = $1, updated_by = $2, updated_date = NOW() WHERE id = $3`, ["N", deletedBy, id] ); logger.info( `외부 호출 설정 삭제 완료: ${existingConfig.config_name} (ID: ${id})` ); } catch (error) { logger.error(`외부 호출 설정 삭제 실패 (ID: ${id}):`, error); throw error; } } /** * 외부 호출 설정 테스트 */ async testConfig(id: number): Promise<{ success: boolean; message: string }> { try { logger.info(`=== 외부 호출 설정 테스트 시작: ID ${id} ===`); const config = await this.getConfigById(id); if (!config) { throw new Error(`외부 호출 설정을 찾을 수 없습니다: ID ${id}`); } // TODO: ExternalCallService를 사용하여 실제 테스트 호출 // 현재는 기본적인 검증만 수행 const configData = config.config_data as any; let isValid = true; let validationMessage = ""; switch (config.api_type) { case "discord": if (!configData.webhookUrl) { isValid = false; validationMessage = "Discord 웹훅 URL이 필요합니다."; } break; case "slack": if (!configData.webhookUrl) { isValid = false; validationMessage = "Slack 웹훅 URL이 필요합니다."; } break; case "kakao-talk": if (!configData.accessToken) { isValid = false; validationMessage = "카카오톡 액세스 토큰이 필요합니다."; } break; default: if (config.call_type === "rest-api" && !configData.url) { isValid = false; validationMessage = "API URL이 필요합니다."; } } if (!isValid) { logger.warn(`외부 호출 설정 테스트 실패: ${validationMessage}`); return { success: false, message: validationMessage }; } logger.info(`외부 호출 설정 테스트 성공: ${config.config_name}`); return { success: true, message: "설정이 유효합니다." }; } catch (error) { logger.error(`외부 호출 설정 테스트 실패 (ID: ${id}):`, error); return { success: false, message: error instanceof Error ? error.message : "테스트 실패", }; } } /** * 🔥 데이터 매핑과 함께 외부호출 실행 */ async executeConfigWithDataMapping( configId: number, requestData: Record, contextData: Record ): Promise<{ success: boolean; message: string; data?: any; executionTime: number; error?: string; }> { const startTime = performance.now(); try { logger.info(`=== 외부호출 실행 시작 (ID: ${configId}) ===`); // 1. 설정 조회 const config = await this.getConfigById(configId); if (!config) { throw new Error(`외부호출 설정을 찾을 수 없습니다: ${configId}`); } // 2. 데이터 매핑 처리 (있는 경우) let processedData = requestData; const configData = config.config_data as any; if (configData?.dataMappingConfig?.outboundMapping) { logger.info("Outbound 데이터 매핑 처리 중..."); processedData = await this.processOutboundMapping( configData.dataMappingConfig.outboundMapping, requestData ); } // 3. 외부 API 호출 const callResult = await this.executeExternalCall( config, processedData, contextData ); // 4. Inbound 데이터 매핑 처리 (있는 경우) if (callResult.success && configData?.dataMappingConfig?.inboundMapping) { logger.info("Inbound 데이터 매핑 처리 중..."); await this.processInboundMapping( configData.dataMappingConfig.inboundMapping, callResult.data ); } const executionTime = performance.now() - startTime; logger.info(`외부호출 실행 완료: ${executionTime.toFixed(2)}ms`); return { success: callResult.success, message: callResult.success ? `외부호출 '${config.config_name}' 실행 완료` : `외부호출 '${config.config_name}' 실행 실패`, data: callResult.data, executionTime, error: callResult.error, }; } catch (error) { const executionTime = performance.now() - startTime; logger.error("외부호출 실행 실패:", error); const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; return { success: false, message: `외부호출 실행 실패: ${errorMessage}`, executionTime, error: errorMessage, }; } } /** * 🔥 버튼 제어용 외부호출 설정 목록 조회 (간소화된 정보) */ async getConfigsForButtonControl(companyCode: string): Promise< Array<{ id: string; name: string; description?: string; apiUrl: string; method: string; hasDataMapping: boolean; }> > { try { const configs = await query<{ id: number; config_name: string; description: string | null; config_data: any; }>( `SELECT id, config_name, description, config_data FROM external_call_configs WHERE company_code = $1 AND is_active = $2 ORDER BY config_name ASC`, [companyCode, "Y"] ); return configs.map((config) => { const configData = config.config_data as any; return { id: config.id.toString(), name: config.config_name, description: config.description || undefined, apiUrl: configData?.restApiSettings?.apiUrl || "", method: configData?.restApiSettings?.httpMethod || "GET", hasDataMapping: !!configData?.dataMappingConfig, }; }); } catch (error) { logger.error("버튼 제어용 외부호출 설정 조회 실패:", error); throw error; } } /** * 🔥 실제 외부 API 호출 실행 */ private async executeExternalCall( config: ExternalCallConfig, requestData: Record, contextData: Record ): Promise<{ success: boolean; data?: any; error?: string }> { try { const configData = config.config_data as any; const restApiSettings = configData?.restApiSettings; if (!restApiSettings) { throw new Error("REST API 설정이 없습니다."); } const { apiUrl, httpMethod, headers = {}, timeout = 30000, } = restApiSettings; // 요청 헤더 준비 const requestHeaders = { "Content-Type": "application/json", ...headers, }; // 인증 처리 if (restApiSettings.authentication?.type === "basic") { const { username, password } = restApiSettings.authentication; const credentials = Buffer.from(`${username}:${password}`).toString( "base64" ); requestHeaders["Authorization"] = `Basic ${credentials}`; } else if (restApiSettings.authentication?.type === "bearer") { const { token } = restApiSettings.authentication; requestHeaders["Authorization"] = `Bearer ${token}`; } // 요청 본문 준비 let requestBody = undefined; if (["POST", "PUT", "PATCH"].includes(httpMethod.toUpperCase())) { requestBody = JSON.stringify({ ...requestData, _context: contextData, // 컨텍스트 정보 추가 }); } logger.info(`외부 API 호출: ${httpMethod} ${apiUrl}`); // 실제 HTTP 요청 (여기서는 간단한 예시) // 실제 구현에서는 axios나 fetch를 사용 const response = await fetch(apiUrl, { method: httpMethod, headers: requestHeaders, body: requestBody, signal: AbortSignal.timeout(timeout), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const responseData = await response.json(); return { success: true, data: responseData, }; } catch (error) { logger.error("외부 API 호출 실패:", error); const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; return { success: false, error: errorMessage, }; } } /** * 🔥 Outbound 데이터 매핑 처리 */ private async processOutboundMapping( mapping: any, sourceData: Record ): Promise> { try { // 간단한 매핑 로직 (실제로는 더 복잡한 변환 로직 필요) const mappedData: Record = {}; if (mapping.fieldMappings) { for (const fieldMapping of mapping.fieldMappings) { const { sourceField, targetField, transformation } = fieldMapping; let value = sourceData[sourceField]; // 변환 로직 적용 if (transformation) { switch (transformation.type) { case "format": // 포맷 변환 로직 break; case "calculate": // 계산 로직 break; default: // 기본값 그대로 사용 break; } } mappedData[targetField] = value; } } return mappedData; } catch (error) { logger.error("Outbound 데이터 매핑 처리 실패:", error); return sourceData; // 실패 시 원본 데이터 반환 } } /** * 🔥 Inbound 데이터 매핑 처리 */ private async processInboundMapping( mapping: any, responseData: any ): Promise { try { // Inbound 매핑 로직 (응답 데이터를 내부 시스템에 저장) logger.info("Inbound 데이터 매핑 처리:", mapping); // 실제 구현에서는 응답 데이터를 파싱하여 내부 테이블에 저장하는 로직 필요 // 예: 외부 API에서 받은 사용자 정보를 내부 사용자 테이블에 업데이트 } catch (error) { logger.error("Inbound 데이터 매핑 처리 실패:", error); // Inbound 매핑 실패는 전체 플로우를 중단하지 않음 } } } export default new ExternalCallConfigService();