import { PrismaClient } from "@prisma/client"; import { logger } from "../utils/logger"; const prisma = new PrismaClient(); // 외부 호출 설정 타입 정의 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 where: any = {}; // 회사 코드 필터 if (filter.company_code) { where.company_code = filter.company_code; } // 호출 타입 필터 if (filter.call_type) { where.call_type = filter.call_type; } // API 타입 필터 if (filter.api_type) { where.api_type = filter.api_type; } // 활성화 상태 필터 if (filter.is_active) { where.is_active = filter.is_active; } // 검색어 필터 (설정 이름 또는 설명) if (filter.search) { where.OR = [ { config_name: { contains: filter.search, mode: "insensitive" } }, { description: { contains: filter.search, mode: "insensitive" } }, ]; } const configs = await prisma.external_call_configs.findMany({ where, orderBy: [{ is_active: "desc" }, { created_date: "desc" }], }); logger.info(`외부 호출 설정 조회 결과: ${configs.length}개`); return configs as ExternalCallConfig[]; } catch (error) { logger.error("외부 호출 설정 목록 조회 실패:", error); throw error; } } /** * 외부 호출 설정 단일 조회 */ async getConfigById(id: number): Promise { try { logger.info(`=== 외부 호출 설정 조회: ID ${id} ===`); const config = await prisma.external_call_configs.findUnique({ where: { id }, }); if (config) { logger.info(`외부 호출 설정 조회 성공: ${config.config_name}`); } else { logger.warn(`외부 호출 설정을 찾을 수 없음: ID ${id}`); } return config as ExternalCallConfig | 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 prisma.external_call_configs.findFirst({ where: { config_name: data.config_name, company_code: data.company_code || "*", is_active: "Y", }, }); if (existingConfig) { throw new Error( `동일한 이름의 외부 호출 설정이 이미 존재합니다: ${data.config_name}` ); } const newConfig = await prisma.external_call_configs.create({ data: { config_name: data.config_name, call_type: data.call_type, api_type: data.api_type, config_data: data.config_data, description: data.description, company_code: data.company_code || "*", is_active: data.is_active || "Y", created_by: data.created_by, updated_by: data.updated_by, }, }); logger.info( `외부 호출 설정 생성 완료: ${newConfig.config_name} (ID: ${newConfig.id})` ); return newConfig as ExternalCallConfig; } 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 prisma.external_call_configs.findFirst({ where: { config_name: data.config_name, company_code: data.company_code || existingConfig.company_code, is_active: "Y", id: { not: id }, }, }); if (duplicateConfig) { throw new Error( `동일한 이름의 외부 호출 설정이 이미 존재합니다: ${data.config_name}` ); } } const updatedConfig = await prisma.external_call_configs.update({ where: { id }, data: { ...(data.config_name && { config_name: data.config_name }), ...(data.call_type && { call_type: data.call_type }), ...(data.api_type !== undefined && { api_type: data.api_type }), ...(data.config_data && { config_data: data.config_data }), ...(data.description !== undefined && { description: data.description, }), ...(data.company_code && { company_code: data.company_code }), ...(data.is_active && { is_active: data.is_active }), ...(data.updated_by && { updated_by: data.updated_by }), updated_date: new Date(), }, }); logger.info( `외부 호출 설정 수정 완료: ${updatedConfig.config_name} (ID: ${id})` ); return updatedConfig as ExternalCallConfig; } 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 prisma.external_call_configs.update({ where: { id }, data: { is_active: "N", updated_by: deletedBy, updated_date: new Date(), }, }); 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> { try { const configs = await prisma.external_call_configs.findMany({ where: { company_code: companyCode, is_active: "Y", }, select: { id: true, config_name: true, description: true, config_data: true, }, orderBy: { config_name: "asc", }, }); 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();