ERP-node/backend-node/src/services/externalCallService.ts

391 lines
11 KiB
TypeScript
Raw Normal View History

2025-09-17 11:47:57 +09:00
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);
}
2025-09-26 17:11:18 +09:00
// 기본 헤더 준비
const headers = { ...(settings.headers || {}) };
// 인증 정보 처리
if (settings.authentication) {
switch (settings.authentication.type) {
case "api-key":
if (settings.authentication.apiKey) {
headers["X-API-Key"] = settings.authentication.apiKey;
}
break;
case "basic":
if (
settings.authentication.username &&
settings.authentication.password
) {
const credentials = Buffer.from(
`${settings.authentication.username}:${settings.authentication.password}`
).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
}
break;
case "bearer":
if (settings.authentication.token) {
headers["Authorization"] =
`Bearer ${settings.authentication.token}`;
}
break;
case "custom":
if (
settings.authentication.headerName &&
settings.authentication.headerValue
) {
headers[settings.authentication.headerName] =
settings.authentication.headerValue;
}
break;
// 'none' 타입은 아무것도 하지 않음
}
}
console.log(`🔐 [ExternalCallService] 인증 처리 완료:`, {
authType: settings.authentication?.type || "none",
hasAuthHeader: !!headers["Authorization"],
headers: Object.keys(headers),
});
2025-09-17 11:47:57 +09:00
return await this.makeHttpRequest({
url: settings.url,
method: settings.method,
2025-09-26 17:11:18 +09:00
headers: headers,
2025-09-17 11:47:57 +09:00
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);
2025-09-26 17:11:18 +09:00
// GET, HEAD 메서드는 body를 가질 수 없음
const method = options.method.toUpperCase();
const requestOptions: RequestInit = {
2025-09-17 11:47:57 +09:00
method: options.method,
headers: options.headers,
signal: controller.signal,
2025-09-26 17:11:18 +09:00
};
// GET, HEAD 메서드가 아닌 경우에만 body 추가
if (method !== "GET" && method !== "HEAD" && options.body) {
requestOptions.body = options.body;
}
const response = await fetch(options.url, requestOptions);
2025-09-17 11:47:57 +09:00
clearTimeout(timeoutId);
const responseText = await response.text();
2025-09-26 17:11:18 +09:00
// 디버깅을 위한 로그 추가
console.log(`🔍 [ExternalCallService] HTTP 응답:`, {
url: options.url,
method: options.method,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
responseText: responseText.substring(0, 500), // 처음 500자만 로그
});
2025-09-17 11:47:57 +09:00
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,
};
}
}