import { ExternalCallConfig, ExternalCallResult, ExternalCallRequest, SlackSettings, KakaoTalkSettings, DiscordSettings, GenericApiSettings, EmailSettings, SupportedExternalCallSettings, TemplateOptions, } from "../types/externalCallTypes"; /** * 외부 호출 서비스 * REST API, 웹훅, 이메일 등 다양한 외부 시스템 호출을 처리 */ export class ExternalCallService { private readonly DEFAULT_TIMEOUT = 30000; // 30초 private readonly DEFAULT_RETRY_COUNT = 3; private readonly DEFAULT_RETRY_DELAY = 1000; // 1초 /** * 외부 호출 실행 */ async executeExternalCall( request: ExternalCallRequest ): Promise { const startTime = Date.now(); try { let result: ExternalCallResult; switch (request.settings.callType) { case "rest-api": result = await this.executeRestApiCall(request); break; case "email": result = await this.executeEmailCall(request); break; case "ftp": throw new Error("FTP 호출은 아직 구현되지 않았습니다."); case "queue": throw new Error("메시지 큐 호출은 아직 구현되지 않았습니다."); default: throw new Error( `지원되지 않는 호출 타입: ${request.settings.callType}` ); } result.executionTime = Date.now() - startTime; result.timestamp = new Date(); return result; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), executionTime: Date.now() - startTime, timestamp: new Date(), }; } } /** * REST API 호출 실행 */ private async executeRestApiCall( request: ExternalCallRequest ): Promise { const settings = request.settings as any; // 임시로 any 사용 switch (settings.apiType) { case "slack": return await this.executeSlackWebhook( settings as SlackSettings, request.templateData ); case "kakao-talk": return await this.executeKakaoTalkApi( settings as KakaoTalkSettings, request.templateData ); case "discord": return await this.executeDiscordWebhook( settings as DiscordSettings, request.templateData ); case "generic": default: return await this.executeGenericApi( settings as GenericApiSettings, request.templateData ); } } /** * 슬랙 웹훅 실행 */ private async executeSlackWebhook( settings: SlackSettings, templateData?: Record ): Promise { const payload = { text: this.processTemplate(settings.message, templateData), channel: settings.channel, username: settings.username || "DataFlow Bot", icon_emoji: settings.iconEmoji || ":robot_face:", }; return await this.makeHttpRequest({ url: settings.webhookUrl, method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), timeout: settings.timeout || this.DEFAULT_TIMEOUT, }); } /** * 카카오톡 API 실행 */ private async executeKakaoTalkApi( settings: KakaoTalkSettings, templateData?: Record ): Promise { const payload = { object_type: "text", text: this.processTemplate(settings.message, templateData), link: { web_url: "https://developers.kakao.com", mobile_web_url: "https://developers.kakao.com", }, }; return await this.makeHttpRequest({ url: "https://kapi.kakao.com/v2/api/talk/memo/default/send", method: "POST", headers: { Authorization: `Bearer ${settings.accessToken}`, "Content-Type": "application/x-www-form-urlencoded", }, body: `template_object=${encodeURIComponent(JSON.stringify(payload))}`, timeout: settings.timeout || this.DEFAULT_TIMEOUT, }); } /** * 디스코드 웹훅 실행 */ private async executeDiscordWebhook( settings: DiscordSettings, templateData?: Record ): Promise { const payload = { content: this.processTemplate(settings.message, templateData), username: settings.username || "시스템 알리미", avatar_url: settings.avatarUrl, }; return await this.makeHttpRequest({ url: settings.webhookUrl, method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), timeout: settings.timeout || this.DEFAULT_TIMEOUT, }); } /** * 일반 REST API 실행 */ private async executeGenericApi( settings: GenericApiSettings, templateData?: Record ): Promise { let body = settings.body; if (body && templateData) { body = this.processTemplate(body, templateData); } return await this.makeHttpRequest({ url: settings.url, method: settings.method, headers: settings.headers || {}, body: body, timeout: settings.timeout || this.DEFAULT_TIMEOUT, }); } /** * 이메일 호출 실행 (향후 구현) */ private async executeEmailCall( request: ExternalCallRequest ): Promise { // TODO: 이메일 발송 구현 (Java MailUtil 연동) throw new Error("이메일 발송 기능은 아직 구현되지 않았습니다."); } /** * HTTP 요청 실행 (공통) */ private async makeHttpRequest(options: { url: string; method: string; headers?: Record; body?: string; timeout: number; }): Promise { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout); const response = await fetch(options.url, { method: options.method, headers: options.headers, body: options.body, signal: controller.signal, }); clearTimeout(timeoutId); const responseText = await response.text(); return { success: response.ok, statusCode: response.status, response: responseText, executionTime: 0, // 상위에서 설정됨 timestamp: new Date(), }; } catch (error) { if (error instanceof Error) { if (error.name === "AbortError") { throw new Error(`요청 시간 초과 (${options.timeout}ms)`); } throw error; } throw new Error(`HTTP 요청 실패: ${String(error)}`); } } /** * 템플릿 문자열 처리 */ private processTemplate( template: string, data?: Record, options: TemplateOptions = {} ): string { if (!data || Object.keys(data).length === 0) { return template; } const startDelimiter = options.startDelimiter || "{{"; const endDelimiter = options.endDelimiter || "}}"; let result = template; Object.entries(data).forEach(([key, value]) => { const placeholder = `${startDelimiter}${key}${endDelimiter}`; const replacement = String(value ?? ""); result = result.replace(new RegExp(placeholder, "g"), replacement); }); return result; } /** * 외부 호출 설정 검증 */ validateSettings(settings: SupportedExternalCallSettings): { valid: boolean; errors: string[]; } { const errors: string[] = []; if (settings.callType === "rest-api") { switch (settings.apiType) { case "slack": const slackSettings = settings as SlackSettings; if (!slackSettings.webhookUrl) errors.push("슬랙 웹훅 URL이 필요합니다."); if (!slackSettings.message) errors.push("슬랙 메시지가 필요합니다."); break; case "kakao-talk": const kakaoSettings = settings as KakaoTalkSettings; if (!kakaoSettings.accessToken) errors.push("카카오톡 액세스 토큰이 필요합니다."); if (!kakaoSettings.message) errors.push("카카오톡 메시지가 필요합니다."); break; case "discord": const discordSettings = settings as DiscordSettings; if (!discordSettings.webhookUrl) errors.push("디스코드 웹훅 URL이 필요합니다."); if (!discordSettings.message) errors.push("디스코드 메시지가 필요합니다."); break; case "generic": default: const genericSettings = settings as GenericApiSettings; if (!genericSettings.url) errors.push("API URL이 필요합니다."); if (!genericSettings.method) errors.push("HTTP 메서드가 필요합니다."); break; } } else if (settings.callType === "email") { const emailSettings = settings as EmailSettings; if (!emailSettings.smtpHost) errors.push("SMTP 호스트가 필요합니다."); if (!emailSettings.toEmail) errors.push("수신 이메일이 필요합니다."); if (!emailSettings.subject) errors.push("이메일 제목이 필요합니다."); } return { valid: errors.length === 0, errors, }; } }