디스코드 웹 훅 테스트 구현
This commit is contained in:
324
backend-node/src/services/externalCallService.ts
Normal file
324
backend-node/src/services/externalCallService.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
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<ExternalCallResult> {
|
||||
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<ExternalCallResult> {
|
||||
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<string, unknown>
|
||||
): Promise<ExternalCallResult> {
|
||||
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<string, unknown>
|
||||
): Promise<ExternalCallResult> {
|
||||
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<string, unknown>
|
||||
): Promise<ExternalCallResult> {
|
||||
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<string, unknown>
|
||||
): Promise<ExternalCallResult> {
|
||||
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<ExternalCallResult> {
|
||||
// TODO: 이메일 발송 구현 (Java MailUtil 연동)
|
||||
throw new Error("이메일 발송 기능은 아직 구현되지 않았습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청 실행 (공통)
|
||||
*/
|
||||
private async makeHttpRequest(options: {
|
||||
url: string;
|
||||
method: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
timeout: number;
|
||||
}): Promise<ExternalCallResult> {
|
||||
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<string, unknown>,
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user