디스코드 웹 훅 테스트 구현
This commit is contained in:
parent
536a975dc7
commit
f85aac65db
|
|
@ -28,6 +28,7 @@ import templateStandardRoutes from "./routes/templateStandardRoutes";
|
|||
import componentStandardRoutes from "./routes/componentStandardRoutes";
|
||||
import layoutRoutes from "./routes/layoutRoutes";
|
||||
import dataRoutes from "./routes/dataRoutes";
|
||||
import externalCallRoutes from "./routes/externalCallRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
|
|
@ -119,6 +120,7 @@ app.use("/api/admin/component-standards", componentStandardRoutes);
|
|||
app.use("/api/layouts", layoutRoutes);
|
||||
app.use("/api/screen", screenStandardRoutes);
|
||||
app.use("/api/data", dataRoutes);
|
||||
app.use("/api/external-calls", externalCallRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import { ExternalCallService } from "../services/externalCallService";
|
||||
import {
|
||||
ExternalCallRequest,
|
||||
SupportedExternalCallSettings,
|
||||
} from "../types/externalCallTypes";
|
||||
|
||||
const router = Router();
|
||||
const externalCallService = new ExternalCallService();
|
||||
|
||||
/**
|
||||
* 외부 호출 테스트
|
||||
* POST /api/external-calls/test
|
||||
*/
|
||||
router.post("/test", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { settings, templateData } = req.body;
|
||||
|
||||
if (!settings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "외부 호출 설정이 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 설정 검증
|
||||
const validation = externalCallService.validateSettings(
|
||||
settings as SupportedExternalCallSettings
|
||||
);
|
||||
if (!validation.valid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "설정 검증 실패",
|
||||
details: validation.errors,
|
||||
});
|
||||
}
|
||||
|
||||
// 테스트 요청 생성
|
||||
const testRequest: ExternalCallRequest = {
|
||||
diagramId: 0, // 테스트용
|
||||
relationshipId: "test",
|
||||
settings: settings as SupportedExternalCallSettings,
|
||||
templateData: templateData || {
|
||||
recordCount: 5,
|
||||
tableName: "test_table",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
// 외부 호출 실행
|
||||
const result = await externalCallService.executeExternalCall(testRequest);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("외부 호출 테스트 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "알 수 없는 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 실행
|
||||
* POST /api/external-calls/execute
|
||||
*/
|
||||
router.post("/execute", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { diagramId, relationshipId, settings, templateData } = req.body;
|
||||
|
||||
if (!diagramId || !relationshipId || !settings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error:
|
||||
"필수 파라미터가 누락되었습니다. (diagramId, relationshipId, settings)",
|
||||
});
|
||||
}
|
||||
|
||||
// 설정 검증
|
||||
const validation = externalCallService.validateSettings(
|
||||
settings as SupportedExternalCallSettings
|
||||
);
|
||||
if (!validation.valid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "설정 검증 실패",
|
||||
details: validation.errors,
|
||||
});
|
||||
}
|
||||
|
||||
// 외부 호출 요청 생성
|
||||
const callRequest: ExternalCallRequest = {
|
||||
diagramId: parseInt(diagramId),
|
||||
relationshipId,
|
||||
settings: settings as SupportedExternalCallSettings,
|
||||
templateData,
|
||||
};
|
||||
|
||||
// 외부 호출 실행
|
||||
const result = await externalCallService.executeExternalCall(callRequest);
|
||||
|
||||
// TODO: 호출 결과를 데이터베이스에 로그로 저장 (향후 구현)
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("외부 호출 실행 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "알 수 없는 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 지원되는 외부 호출 타입 목록 조회
|
||||
* GET /api/external-calls/types
|
||||
*/
|
||||
router.get("/types", (req: Request, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
supportedTypes: {
|
||||
"rest-api": {
|
||||
name: "REST API 호출",
|
||||
subtypes: {
|
||||
slack: "슬랙 웹훅",
|
||||
"kakao-talk": "카카오톡 알림",
|
||||
discord: "디스코드 웹훅",
|
||||
generic: "일반 REST API",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
name: "이메일 전송",
|
||||
status: "구현 예정",
|
||||
},
|
||||
ftp: {
|
||||
name: "FTP 업로드",
|
||||
status: "구현 예정",
|
||||
},
|
||||
queue: {
|
||||
name: "메시지 큐",
|
||||
status: "구현 예정",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 검증
|
||||
* POST /api/external-calls/validate
|
||||
*/
|
||||
router.post("/validate", (req: Request, res: Response) => {
|
||||
try {
|
||||
const { settings } = req.body;
|
||||
|
||||
if (!settings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "검증할 설정이 필요합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const validation = externalCallService.validateSettings(
|
||||
settings as SupportedExternalCallSettings
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
validation,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("설정 검증 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "검증 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* 외부 호출 관련 타입 정의
|
||||
*/
|
||||
|
||||
// 기본 외부 호출 설정
|
||||
export interface ExternalCallConfig {
|
||||
callType: "rest-api" | "email" | "ftp" | "queue";
|
||||
apiType?: "slack" | "kakao-talk" | "discord" | "generic";
|
||||
|
||||
// 공통 설정
|
||||
timeout?: number; // ms
|
||||
retryCount?: number;
|
||||
retryDelay?: number; // ms
|
||||
}
|
||||
|
||||
// REST API 공통 설정
|
||||
export interface RestApiConfig extends ExternalCallConfig {
|
||||
callType: "rest-api";
|
||||
url: string;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
// 슬랙 웹훅 설정
|
||||
export interface SlackSettings extends ExternalCallConfig {
|
||||
callType: "rest-api";
|
||||
apiType: "slack";
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
message: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
}
|
||||
|
||||
// 카카오톡 API 설정
|
||||
export interface KakaoTalkSettings extends ExternalCallConfig {
|
||||
callType: "rest-api";
|
||||
apiType: "kakao-talk";
|
||||
accessToken: string;
|
||||
message: string;
|
||||
templateId?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
// 디스코드 웹훅 설정
|
||||
export interface DiscordSettings extends ExternalCallConfig {
|
||||
callType: "rest-api";
|
||||
apiType: "discord";
|
||||
webhookUrl: string;
|
||||
message: string;
|
||||
username?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
// 일반 REST API 설정
|
||||
export interface GenericApiSettings extends ExternalCallConfig {
|
||||
callType: "rest-api";
|
||||
apiType: "generic";
|
||||
url: string;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
// 이메일 설정
|
||||
export interface EmailSettings extends ExternalCallConfig {
|
||||
callType: "email";
|
||||
smtpHost: string;
|
||||
smtpPort: number;
|
||||
smtpUser: string;
|
||||
smtpPass: string;
|
||||
fromEmail: string;
|
||||
toEmail: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
// 외부 호출 실행 결과
|
||||
export interface ExternalCallResult {
|
||||
success: boolean;
|
||||
statusCode?: number;
|
||||
response?: string;
|
||||
error?: string;
|
||||
executionTime: number; // ms
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// 외부 호출 실행 요청
|
||||
export interface ExternalCallRequest {
|
||||
diagramId: number;
|
||||
relationshipId: string;
|
||||
settings: ExternalCallConfig;
|
||||
templateData?: Record<string, unknown>; // 템플릿 변수 데이터
|
||||
}
|
||||
|
||||
// 템플릿 처리 옵션
|
||||
export interface TemplateOptions {
|
||||
startDelimiter?: string; // 기본값: "{{"
|
||||
endDelimiter?: string; // 기본값: "}}"
|
||||
escapeHtml?: boolean; // 기본값: false
|
||||
}
|
||||
|
||||
// 외부 호출 로그 (향후 구현)
|
||||
export interface ExternalCallLog {
|
||||
id: number;
|
||||
diagramId: number;
|
||||
relationshipId: string;
|
||||
callType: string;
|
||||
apiType?: string;
|
||||
targetUrl: string;
|
||||
requestPayload?: string;
|
||||
responseStatus?: number;
|
||||
responseBody?: string;
|
||||
errorMessage?: string;
|
||||
executionTimeMs: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// 지원되는 외부 호출 타입들의 Union 타입
|
||||
export type SupportedExternalCallSettings =
|
||||
| SlackSettings
|
||||
| KakaoTalkSettings
|
||||
| DiscordSettings
|
||||
| GenericApiSettings
|
||||
| EmailSettings;
|
||||
|
||||
|
|
@ -5,6 +5,9 @@ import { Label } from "@/components/ui/label";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalCallAPI } from "@/lib/api/externalCall";
|
||||
import { toast } from "sonner";
|
||||
import { Globe } from "lucide-react";
|
||||
import { ExternalCallSettings as ExternalCallSettingsType } from "@/types/connectionTypes";
|
||||
|
||||
|
|
@ -13,13 +16,129 @@ interface ExternalCallSettingsProps {
|
|||
onSettingsChange: (settings: ExternalCallSettingsType) => void;
|
||||
}
|
||||
|
||||
const handleTestExternalCall = async (settings: ExternalCallSettingsType) => {
|
||||
let loadingToastId: string | number | undefined;
|
||||
|
||||
try {
|
||||
// 설정을 백엔드 형식으로 변환
|
||||
const backendSettings: Record<string, unknown> = {
|
||||
callType: settings.callType,
|
||||
timeout: 10000, // 10초 타임아웃 설정
|
||||
};
|
||||
|
||||
if (settings.callType === "rest-api") {
|
||||
backendSettings.apiType = settings.apiType;
|
||||
|
||||
switch (settings.apiType) {
|
||||
case "slack":
|
||||
backendSettings.webhookUrl = settings.slackWebhookUrl;
|
||||
backendSettings.message =
|
||||
settings.slackMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
|
||||
backendSettings.channel = settings.slackChannel;
|
||||
break;
|
||||
case "kakao-talk":
|
||||
backendSettings.accessToken = settings.kakaoAccessToken;
|
||||
backendSettings.message =
|
||||
settings.kakaoMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
|
||||
break;
|
||||
case "discord":
|
||||
backendSettings.webhookUrl = settings.discordWebhookUrl;
|
||||
backendSettings.message =
|
||||
settings.discordMessage || "테스트 메시지: {{recordCount}}건의 데이터가 처리되었습니다.";
|
||||
backendSettings.username = settings.discordUsername;
|
||||
break;
|
||||
case "generic":
|
||||
default:
|
||||
backendSettings.url = settings.apiUrl;
|
||||
backendSettings.method = settings.httpMethod || "POST";
|
||||
try {
|
||||
backendSettings.headers = settings.headers ? JSON.parse(settings.headers) : {};
|
||||
} catch (error) {
|
||||
console.warn("Headers JSON 파싱 실패, 기본값 사용:", error);
|
||||
backendSettings.headers = {};
|
||||
}
|
||||
backendSettings.body = settings.bodyTemplate || "{}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 로딩 토스트 시작
|
||||
loadingToastId = toast.loading("외부 호출 테스트 중...", {
|
||||
duration: 12000, // 12초 후 자동으로 사라짐
|
||||
});
|
||||
|
||||
// 타임아웃을 위한 Promise.race 사용
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("테스트 요청이 10초 내에 완료되지 않았습니다.")), 10000);
|
||||
});
|
||||
|
||||
const testPromise = ExternalCallAPI.testExternalCall({
|
||||
settings: backendSettings,
|
||||
templateData: {
|
||||
recordCount: 5,
|
||||
tableName: "test_table",
|
||||
timestamp: new Date().toISOString(),
|
||||
message: "데이터플로우 테스트 실행",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await Promise.race([testPromise, timeoutPromise]);
|
||||
|
||||
// 로딩 토스트 제거
|
||||
if (loadingToastId) {
|
||||
toast.dismiss(loadingToastId);
|
||||
}
|
||||
|
||||
if (result.success && result.result?.success) {
|
||||
toast.success("외부 호출 테스트 성공!", {
|
||||
description: `응답 시간: ${result.result.executionTime}ms`,
|
||||
duration: 4000,
|
||||
});
|
||||
} else {
|
||||
toast.error("외부 호출 테스트 실패", {
|
||||
description: result.result?.error || result.error || "알 수 없는 오류",
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테스트 실행 중 오류:", error);
|
||||
|
||||
// 로딩 토스트 제거
|
||||
if (loadingToastId) {
|
||||
toast.dismiss(loadingToastId);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
toast.error("테스트 실행 중 오류가 발생했습니다.", {
|
||||
description: error.message,
|
||||
duration: 6000,
|
||||
});
|
||||
} else {
|
||||
toast.error("테스트 실행 중 알 수 없는 오류가 발생했습니다.", {
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ settings, onSettingsChange }) => {
|
||||
return (
|
||||
<div className="rounded-lg border border-l-4 border-l-orange-500 bg-orange-50/30 p-4">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-sm font-medium">외부 호출 설정</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleTestExternalCall(settings)}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
테스트
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="callType" className="text-sm">
|
||||
|
|
@ -147,7 +266,7 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
|
|||
<>
|
||||
<div>
|
||||
<Label htmlFor="discordWebhookUrl" className="text-sm">
|
||||
디스코드 웹훅 URL
|
||||
디스코드 웹훅 URL <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="discordWebhookUrl"
|
||||
|
|
@ -157,6 +276,18 @@ export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ sett
|
|||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discordUsername" className="text-sm">
|
||||
이름
|
||||
</Label>
|
||||
<Input
|
||||
id="discordUsername"
|
||||
value={settings.discordUsername || ""}
|
||||
onChange={(e) => onSettingsChange({ ...settings, discordUsername: e.target.value })}
|
||||
placeholder="ERP 시스템"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discordMessage" className="text-sm">
|
||||
메시지 내용
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* 외부 호출 API 클라이언트
|
||||
*/
|
||||
|
||||
// 백엔드 타입과 동일한 인터페이스 정의
|
||||
export interface ExternalCallResult {
|
||||
success: boolean;
|
||||
statusCode?: number;
|
||||
response?: string;
|
||||
error?: string;
|
||||
executionTime: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ExternalCallTestRequest {
|
||||
settings: Record<string, unknown>;
|
||||
templateData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ExternalCallExecuteRequest {
|
||||
diagramId: number;
|
||||
relationshipId: string;
|
||||
settings: Record<string, unknown>;
|
||||
templateData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 API 클래스
|
||||
*/
|
||||
// API URL 동적 설정 - 기존 client.ts와 동일한 로직
|
||||
const getApiBaseUrl = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentHost = window.location.hostname;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
// 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080
|
||||
if (
|
||||
(currentHost === "localhost" || currentHost === "127.0.0.1") &&
|
||||
(currentPort === "9771" || currentPort === "3000")
|
||||
) {
|
||||
return "http://localhost:8080/api";
|
||||
}
|
||||
|
||||
// 서버 환경에서 localhost:5555 → 39.117.244.52:8080
|
||||
if ((currentHost === "localhost" || currentHost === "127.0.0.1") && currentPort === "5555") {
|
||||
return "http://39.117.244.52:8080/api";
|
||||
}
|
||||
|
||||
// 기타 서버 환경 (내부/외부 IP): → 39.117.244.52:8080
|
||||
return "http://39.117.244.52:8080/api";
|
||||
}
|
||||
|
||||
// 서버 사이드 렌더링 기본값
|
||||
return "http://39.117.244.52:8080/api";
|
||||
};
|
||||
|
||||
export class ExternalCallAPI {
|
||||
private static readonly BASE_URL = `${getApiBaseUrl()}/external-calls`;
|
||||
|
||||
/**
|
||||
* 외부 호출 테스트 실행
|
||||
*/
|
||||
static async testExternalCall(request: ExternalCallTestRequest): Promise<{
|
||||
success: boolean;
|
||||
result?: ExternalCallResult;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${this.BASE_URL}/test`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
// 응답이 JSON인지 확인
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const text = await response.text();
|
||||
throw new Error(`서버에서 JSON이 아닌 응답을 반환했습니다: ${text.substring(0, 100)}...`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("외부 호출 테스트 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 실행
|
||||
*/
|
||||
static async executeExternalCall(request: ExternalCallExecuteRequest): Promise<{
|
||||
success: boolean;
|
||||
result?: ExternalCallResult;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${this.BASE_URL}/execute`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("외부 호출 실행 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원되는 외부 호출 타입 목록 조회
|
||||
*/
|
||||
static async getSupportedTypes(): Promise<{
|
||||
success: boolean;
|
||||
supportedTypes?: Record<string, any>;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${this.BASE_URL}/types`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("지원 타입 조회 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 검증
|
||||
*/
|
||||
static async validateSettings(settings: Record<string, unknown>): Promise<{
|
||||
success: boolean;
|
||||
validation?: ValidationResult;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${this.BASE_URL}/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ settings }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("설정 검증 실패:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,7 @@ const nextConfig = {
|
|||
outputFileTracingRoot: undefined,
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
// 개발 환경과 운영 환경에 따른 백엔드 URL 설정
|
||||
const backendUrl = process.env.NODE_ENV === "development" ? "http://localhost:3000" : "http://backend:8080";
|
||||
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: `${backendUrl}/api/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
// 프록시 설정 제거 - 모든 API가 직접 백엔드 호출
|
||||
|
||||
// 개발 환경에서 CORS 처리
|
||||
async headers() {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export interface ExternalCallSettings {
|
|||
// 디스코드 전용 설정
|
||||
discordWebhookUrl?: string;
|
||||
discordMessage?: string;
|
||||
discordUsername?: string;
|
||||
}
|
||||
|
||||
// ConnectionSetupModal Props 타입
|
||||
|
|
|
|||
Loading…
Reference in New Issue