외부 호출 중간 저장
This commit is contained in:
parent
f85aac65db
commit
b1a3ba713a
|
|
@ -20,6 +20,25 @@ model dynamic_form_data {
|
|||
company_code String @db.VarChar(20)
|
||||
}
|
||||
|
||||
model external_call_configs {
|
||||
id Int @id @default(autoincrement())
|
||||
config_name String @db.VarChar(100)
|
||||
call_type String @db.VarChar(20)
|
||||
api_type String? @db.VarChar(20)
|
||||
config_data Json
|
||||
description String? @db.Text
|
||||
company_code String @default("*") @db.VarChar(20)
|
||||
is_active String @default("Y") @db.Char(1)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @updatedAt @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
@@index([company_code])
|
||||
@@index([call_type, api_type])
|
||||
@@index([is_active])
|
||||
}
|
||||
|
||||
model admin_supply_mng {
|
||||
objid Decimal @id @default(0) @db.Decimal
|
||||
supply_code String? @default("NULL::character varying") @db.VarChar(100)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import componentStandardRoutes from "./routes/componentStandardRoutes";
|
|||
import layoutRoutes from "./routes/layoutRoutes";
|
||||
import dataRoutes from "./routes/dataRoutes";
|
||||
import externalCallRoutes from "./routes/externalCallRoutes";
|
||||
import externalCallConfigRoutes from "./routes/externalCallConfigRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
|
|
@ -121,6 +122,7 @@ app.use("/api/layouts", layoutRoutes);
|
|||
app.use("/api/screen", screenStandardRoutes);
|
||||
app.use("/api/data", dataRoutes);
|
||||
app.use("/api/external-calls", externalCallRoutes);
|
||||
app.use("/api/external-call-configs", externalCallConfigRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import externalCallConfigService, {
|
||||
ExternalCallConfigFilter,
|
||||
} from "../services/externalCallConfigService";
|
||||
import { logger } from "../utils/logger";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 목록 조회
|
||||
* GET /api/external-call-configs
|
||||
*/
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const filter: ExternalCallConfigFilter = {
|
||||
company_code: req.query.company_code as string,
|
||||
call_type: req.query.call_type as string,
|
||||
api_type: req.query.api_type as string,
|
||||
is_active: (req.query.is_active as string) || "Y",
|
||||
search: req.query.search as string,
|
||||
};
|
||||
|
||||
const configs = await externalCallConfigService.getConfigs(filter);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: configs,
|
||||
message: `외부 호출 설정 ${configs.length}개 조회 완료`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 목록 조회 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "외부 호출 설정 목록 조회 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_LIST_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 단일 조회
|
||||
* GET /api/external-call-configs/:id
|
||||
*/
|
||||
router.get("/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 설정 ID입니다.",
|
||||
errorCode: "INVALID_CONFIG_ID",
|
||||
});
|
||||
}
|
||||
|
||||
const config = await externalCallConfigService.getConfigById(id);
|
||||
if (!config) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "외부 호출 설정을 찾을 수 없습니다.",
|
||||
errorCode: "CONFIG_NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: config,
|
||||
message: "외부 호출 설정 조회 완료",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 조회 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "외부 호출 설정 조회 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_GET_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 생성
|
||||
* POST /api/external-call-configs
|
||||
*/
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
config_name,
|
||||
call_type,
|
||||
api_type,
|
||||
config_data,
|
||||
description,
|
||||
company_code,
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!config_name || !call_type || !config_data) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"필수 필드가 누락되었습니다. (config_name, call_type, config_data)",
|
||||
errorCode: "MISSING_REQUIRED_FIELDS",
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const userInfo = (req as any).user;
|
||||
const userId = userInfo?.userId || "SYSTEM";
|
||||
|
||||
const newConfig = await externalCallConfigService.createConfig({
|
||||
config_name,
|
||||
call_type,
|
||||
api_type,
|
||||
config_data,
|
||||
description,
|
||||
company_code: company_code || "*",
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
data: newConfig,
|
||||
message: "외부 호출 설정이 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 생성 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "외부 호출 설정 생성 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_CREATE_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 수정
|
||||
* PUT /api/external-call-configs/:id
|
||||
*/
|
||||
router.put("/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 설정 ID입니다.",
|
||||
errorCode: "INVALID_CONFIG_ID",
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const userInfo = (req as any).user;
|
||||
const userId = userInfo?.userId || "SYSTEM";
|
||||
|
||||
const updatedConfig = await externalCallConfigService.updateConfig(id, {
|
||||
...req.body,
|
||||
updated_by: userId,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: updatedConfig,
|
||||
message: "외부 호출 설정이 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 수정 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "외부 호출 설정 수정 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_UPDATE_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 삭제 (논리 삭제)
|
||||
* DELETE /api/external-call-configs/:id
|
||||
*/
|
||||
router.delete("/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 설정 ID입니다.",
|
||||
errorCode: "INVALID_CONFIG_ID",
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const userInfo = (req as any).user;
|
||||
const userId = userInfo?.userId || "SYSTEM";
|
||||
|
||||
await externalCallConfigService.deleteConfig(id, userId);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "외부 호출 설정이 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 삭제 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "외부 호출 설정 삭제 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_DELETE_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 테스트
|
||||
* POST /api/external-call-configs/:id/test
|
||||
*/
|
||||
router.post("/:id/test", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 설정 ID입니다.",
|
||||
errorCode: "INVALID_CONFIG_ID",
|
||||
});
|
||||
}
|
||||
|
||||
const testResult = await externalCallConfigService.testConfig(id);
|
||||
|
||||
return res.json({
|
||||
success: testResult.success,
|
||||
message: testResult.message,
|
||||
data: testResult,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("외부 호출 설정 테스트 API 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "외부 호출 설정 테스트 실패",
|
||||
errorCode: "EXTERNAL_CALL_CONFIG_TEST_ERROR",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
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<ExternalCallConfig[]> {
|
||||
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<ExternalCallConfig | null> {
|
||||
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<ExternalCallConfig> {
|
||||
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<ExternalCallConfig>
|
||||
): Promise<ExternalCallConfig> {
|
||||
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<void> {
|
||||
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 : "테스트 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ExternalCallConfigService();
|
||||
|
|
@ -124,4 +124,3 @@ export type SupportedExternalCallSettings =
|
|||
| DiscordSettings
|
||||
| GenericApiSettings
|
||||
| EmailSettings;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,401 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, Search, Edit, Trash2, TestTube, Filter } from "lucide-react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ExternalCallConfigAPI,
|
||||
ExternalCallConfig,
|
||||
ExternalCallConfigFilter,
|
||||
CALL_TYPE_OPTIONS,
|
||||
API_TYPE_OPTIONS,
|
||||
ACTIVE_STATUS_OPTIONS,
|
||||
} from "@/lib/api/externalCallConfig";
|
||||
import { ExternalCallConfigModal } from "@/components/admin/ExternalCallConfigModal";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
export default function ExternalCallConfigsPage() {
|
||||
const [configs, setConfigs] = useState<ExternalCallConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filter, setFilter] = useState<ExternalCallConfigFilter>({
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
// 모달 상태
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingConfig, setEditingConfig] = useState<ExternalCallConfig | null>(null);
|
||||
|
||||
// 삭제 확인 다이얼로그 상태
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [configToDelete, setConfigToDelete] = useState<ExternalCallConfig | null>(null);
|
||||
|
||||
// 외부 호출 설정 목록 조회
|
||||
const fetchConfigs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await ExternalCallConfigAPI.getConfigs({
|
||||
...filter,
|
||||
search: searchQuery.trim() || undefined,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setConfigs(response.data || []);
|
||||
} else {
|
||||
toast.error(response.message || "외부 호출 설정 조회 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 조회 오류:", error);
|
||||
toast.error("외부 호출 설정 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 초기 로드 및 필터/검색 변경 시 재조회
|
||||
useEffect(() => {
|
||||
fetchConfigs();
|
||||
}, [filter]);
|
||||
|
||||
// 검색 실행
|
||||
const handleSearch = () => {
|
||||
fetchConfigs();
|
||||
};
|
||||
|
||||
// 검색 입력 시 엔터키 처리
|
||||
const handleSearchKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
// 새 설정 추가
|
||||
const handleAddConfig = () => {
|
||||
setEditingConfig(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// 설정 편집
|
||||
const handleEditConfig = (config: ExternalCallConfig) => {
|
||||
setEditingConfig(config);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// 설정 삭제 확인
|
||||
const handleDeleteConfig = (config: ExternalCallConfig) => {
|
||||
setConfigToDelete(config);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 설정 삭제 실행
|
||||
const confirmDeleteConfig = async () => {
|
||||
if (!configToDelete?.id) return;
|
||||
|
||||
try {
|
||||
const response = await ExternalCallConfigAPI.deleteConfig(configToDelete.id);
|
||||
|
||||
if (response.success) {
|
||||
toast.success("외부 호출 설정이 삭제되었습니다.");
|
||||
fetchConfigs();
|
||||
} else {
|
||||
toast.error(response.message || "외부 호출 설정 삭제 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 삭제 오류:", error);
|
||||
toast.error("외부 호출 설정 삭제 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setDeleteDialogOpen(false);
|
||||
setConfigToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 설정 테스트
|
||||
const handleTestConfig = async (config: ExternalCallConfig) => {
|
||||
if (!config.id) return;
|
||||
|
||||
try {
|
||||
const response = await ExternalCallConfigAPI.testConfig(config.id);
|
||||
|
||||
if (response.success && response.data?.success) {
|
||||
toast.success(`테스트 성공: ${response.data.message}`);
|
||||
} else {
|
||||
toast.error(`테스트 실패: ${response.data?.message || response.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 테스트 오류:", error);
|
||||
toast.error("외부 호출 설정 테스트 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 모달 저장 완료 시 목록 새로고침
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false);
|
||||
setEditingConfig(null);
|
||||
fetchConfigs();
|
||||
};
|
||||
|
||||
// 호출 타입 라벨 가져오기
|
||||
const getCallTypeLabel = (callType: string) => {
|
||||
return CALL_TYPE_OPTIONS.find((option) => option.value === callType)?.label || callType;
|
||||
};
|
||||
|
||||
// API 타입 라벨 가져오기
|
||||
const getApiTypeLabel = (apiType?: string) => {
|
||||
if (!apiType) return "";
|
||||
return API_TYPE_OPTIONS.find((option) => option.value === apiType)?.label || apiType;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">외부 호출 관리</h1>
|
||||
<p className="text-muted-foreground mt-1">Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.</p>
|
||||
</div>
|
||||
<Button onClick={handleAddConfig} className="flex items-center gap-2">
|
||||
<Plus size={16} />새 외부 호출 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Filter size={18} />
|
||||
검색 및 필터
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 검색 */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="설정 이름 또는 설명으로 검색..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyPress={handleSearchKeyPress}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} variant="outline">
|
||||
<Search size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 필터 */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">호출 타입</label>
|
||||
<Select
|
||||
value={filter.call_type || "all"}
|
||||
onValueChange={(value) =>
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
call_type: value === "all" ? undefined : value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
{CALL_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">API 타입</label>
|
||||
<Select
|
||||
value={filter.api_type || "all"}
|
||||
onValueChange={(value) =>
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
api_type: value === "all" ? undefined : value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
{API_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium">상태</label>
|
||||
<Select
|
||||
value={filter.is_active || "Y"}
|
||||
onValueChange={(value) =>
|
||||
setFilter((prev) => ({
|
||||
...prev,
|
||||
is_active: value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 설정 목록 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>외부 호출 설정 목록</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
// 로딩 상태
|
||||
<div className="py-8 text-center">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
) : configs.length === 0 ? (
|
||||
// 빈 상태
|
||||
<div className="py-12 text-center">
|
||||
<div className="text-muted-foreground">
|
||||
<Plus size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p className="text-lg font-medium">등록된 외부 호출 설정이 없습니다.</p>
|
||||
<p className="text-sm">새 외부 호출을 추가해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 설정 테이블 목록
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>설정명</TableHead>
|
||||
<TableHead>호출 타입</TableHead>
|
||||
<TableHead>API 타입</TableHead>
|
||||
<TableHead>설명</TableHead>
|
||||
<TableHead>상태</TableHead>
|
||||
<TableHead>생성일</TableHead>
|
||||
<TableHead className="text-center">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{configs.map((config) => (
|
||||
<TableRow key={config.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-medium">{config.config_name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{getCallTypeLabel(config.call_type)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{config.api_type ? (
|
||||
<Badge variant="secondary">{getApiTypeLabel(config.api_type)}</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-xs">
|
||||
{config.description ? (
|
||||
<span className="text-muted-foreground block truncate text-sm" title={config.description}>
|
||||
{config.description}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-sm">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={config.is_active === "Y" ? "default" : "destructive"}>
|
||||
{config.is_active === "Y" ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-center gap-1">
|
||||
<Button size="sm" variant="outline" onClick={() => handleTestConfig(config)} title="테스트">
|
||||
<TestTube size={14} />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleEditConfig(config)} title="편집">
|
||||
<Edit size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteConfig(config)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 외부 호출 설정 모달 */}
|
||||
<ExternalCallConfigModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleModalSave}
|
||||
editingConfig={editingConfig}
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>외부 호출 설정 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{configToDelete?.config_name}" 설정을 삭제하시겠습니까?
|
||||
<br />이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDeleteConfig} className="bg-destructive hover:bg-destructive/90">
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,522 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
ExternalCallConfigAPI,
|
||||
ExternalCallConfig,
|
||||
CALL_TYPE_OPTIONS,
|
||||
API_TYPE_OPTIONS,
|
||||
ACTIVE_STATUS_OPTIONS,
|
||||
} from "@/lib/api/externalCallConfig";
|
||||
|
||||
interface ExternalCallConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
editingConfig?: ExternalCallConfig | null;
|
||||
}
|
||||
|
||||
export function ExternalCallConfigModal({ isOpen, onClose, onSave, editingConfig }: ExternalCallConfigModalProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<ExternalCallConfig>>({
|
||||
config_name: "",
|
||||
call_type: "rest-api",
|
||||
api_type: "discord",
|
||||
description: "",
|
||||
is_active: "Y",
|
||||
config_data: {},
|
||||
});
|
||||
|
||||
// Discord 설정 상태
|
||||
const [discordSettings, setDiscordSettings] = useState({
|
||||
webhookUrl: "",
|
||||
username: "",
|
||||
avatarUrl: "",
|
||||
});
|
||||
|
||||
// Slack 설정 상태
|
||||
const [slackSettings, setSlackSettings] = useState({
|
||||
webhookUrl: "",
|
||||
channel: "",
|
||||
username: "",
|
||||
});
|
||||
|
||||
// 카카오톡 설정 상태
|
||||
const [kakaoSettings, setKakaoSettings] = useState({
|
||||
accessToken: "",
|
||||
templateId: "",
|
||||
});
|
||||
|
||||
// 일반 API 설정 상태
|
||||
const [genericSettings, setGenericSettings] = useState({
|
||||
url: "",
|
||||
method: "POST",
|
||||
headers: "{}",
|
||||
timeout: "30000",
|
||||
});
|
||||
|
||||
// 편집 모드일 때 기존 데이터 로드
|
||||
useEffect(() => {
|
||||
if (isOpen && editingConfig) {
|
||||
setFormData({
|
||||
config_name: editingConfig.config_name,
|
||||
call_type: editingConfig.call_type,
|
||||
api_type: editingConfig.api_type,
|
||||
description: editingConfig.description || "",
|
||||
is_active: editingConfig.is_active || "Y",
|
||||
config_data: editingConfig.config_data,
|
||||
});
|
||||
|
||||
// API 타입별 설정 데이터 로드
|
||||
const configData = editingConfig.config_data as any;
|
||||
|
||||
if (editingConfig.api_type === "discord") {
|
||||
setDiscordSettings({
|
||||
webhookUrl: configData.webhookUrl || "",
|
||||
username: configData.username || "",
|
||||
avatarUrl: configData.avatarUrl || "",
|
||||
});
|
||||
} else if (editingConfig.api_type === "slack") {
|
||||
setSlackSettings({
|
||||
webhookUrl: configData.webhookUrl || "",
|
||||
channel: configData.channel || "",
|
||||
username: configData.username || "",
|
||||
});
|
||||
} else if (editingConfig.api_type === "kakao-talk") {
|
||||
setKakaoSettings({
|
||||
accessToken: configData.accessToken || "",
|
||||
templateId: configData.templateId || "",
|
||||
});
|
||||
} else if (editingConfig.api_type === "generic") {
|
||||
setGenericSettings({
|
||||
url: configData.url || "",
|
||||
method: configData.method || "POST",
|
||||
headers: JSON.stringify(configData.headers || {}, null, 2),
|
||||
timeout: String(configData.timeout || 30000),
|
||||
});
|
||||
}
|
||||
} else if (isOpen && !editingConfig) {
|
||||
// 새 설정 추가 시 초기화
|
||||
setFormData({
|
||||
config_name: "",
|
||||
call_type: "rest-api",
|
||||
api_type: "discord",
|
||||
description: "",
|
||||
is_active: "Y",
|
||||
config_data: {},
|
||||
});
|
||||
setDiscordSettings({ webhookUrl: "", username: "", avatarUrl: "" });
|
||||
setSlackSettings({ webhookUrl: "", channel: "", username: "" });
|
||||
setKakaoSettings({ accessToken: "", templateId: "" });
|
||||
setGenericSettings({ url: "", method: "POST", headers: "{}", timeout: "30000" });
|
||||
}
|
||||
}, [isOpen, editingConfig]);
|
||||
|
||||
// 호출 타입 변경 시 API 타입 초기화
|
||||
const handleCallTypeChange = (callType: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
call_type: callType,
|
||||
api_type: callType === "rest-api" ? "discord" : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
// config_data 생성
|
||||
const generateConfigData = () => {
|
||||
const { api_type } = formData;
|
||||
|
||||
switch (api_type) {
|
||||
case "discord":
|
||||
return {
|
||||
webhookUrl: discordSettings.webhookUrl,
|
||||
username: discordSettings.username || "ERP 시스템",
|
||||
avatarUrl: discordSettings.avatarUrl || null,
|
||||
};
|
||||
|
||||
case "slack":
|
||||
return {
|
||||
webhookUrl: slackSettings.webhookUrl,
|
||||
channel: slackSettings.channel,
|
||||
username: slackSettings.username || "ERP Bot",
|
||||
};
|
||||
|
||||
case "kakao-talk":
|
||||
return {
|
||||
accessToken: kakaoSettings.accessToken,
|
||||
templateId: kakaoSettings.templateId,
|
||||
};
|
||||
|
||||
case "generic":
|
||||
try {
|
||||
return {
|
||||
url: genericSettings.url,
|
||||
method: genericSettings.method,
|
||||
headers: JSON.parse(genericSettings.headers),
|
||||
timeout: parseInt(genericSettings.timeout),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error("헤더 JSON 형식이 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// 폼 검증
|
||||
const validateForm = () => {
|
||||
if (!formData.config_name?.trim()) {
|
||||
toast.error("설정 이름을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formData.call_type) {
|
||||
toast.error("호출 타입을 선택해주세요.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// REST API인 경우 API 타입별 검증
|
||||
if (formData.call_type === "rest-api") {
|
||||
switch (formData.api_type) {
|
||||
case "discord":
|
||||
if (!discordSettings.webhookUrl.trim()) {
|
||||
toast.error("Discord 웹훅 URL을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case "slack":
|
||||
if (!slackSettings.webhookUrl.trim()) {
|
||||
toast.error("Slack 웹훅 URL을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case "kakao-talk":
|
||||
if (!kakaoSettings.accessToken.trim()) {
|
||||
toast.error("카카오톡 액세스 토큰을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case "generic":
|
||||
if (!genericSettings.url.trim()) {
|
||||
toast.error("API URL을 입력해주세요.");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSON.parse(genericSettings.headers);
|
||||
} catch {
|
||||
toast.error("헤더 JSON 형식이 올바르지 않습니다.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 저장 처리
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const configData = generateConfigData();
|
||||
const saveData = {
|
||||
...formData,
|
||||
config_data: configData,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (editingConfig?.id) {
|
||||
response = await ExternalCallConfigAPI.updateConfig(editingConfig.id, saveData);
|
||||
} else {
|
||||
response = await ExternalCallConfigAPI.createConfig(saveData as any);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
toast.success(editingConfig ? "외부 호출 설정이 수정되었습니다." : "외부 호출 설정이 생성되었습니다.");
|
||||
onSave();
|
||||
} else {
|
||||
toast.error(response.message || "저장 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 저장 오류:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingConfig ? "외부 호출 설정 편집" : "새 외부 호출 설정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="config_name">설정 이름 *</Label>
|
||||
<Input
|
||||
id="config_name"
|
||||
value={formData.config_name}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, config_name: e.target.value }))}
|
||||
placeholder="예: 개발팀 Discord"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="이 외부 호출 설정에 대한 설명을 입력하세요."
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="call_type">호출 타입 *</Label>
|
||||
<Select value={formData.call_type} onValueChange={handleCallTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CALL_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="is_active">상태</Label>
|
||||
<Select
|
||||
value={formData.is_active}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, is_active: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* REST API 설정 */}
|
||||
{formData.call_type === "rest-api" && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="api_type">API 타입 *</Label>
|
||||
<Select
|
||||
value={formData.api_type}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, api_type: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{API_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Discord 설정 */}
|
||||
{formData.api_type === "discord" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">Discord 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="discord_webhook">웹훅 URL *</Label>
|
||||
<Input
|
||||
id="discord_webhook"
|
||||
value={discordSettings.webhookUrl}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discord_username">사용자명</Label>
|
||||
<Input
|
||||
id="discord_username"
|
||||
value={discordSettings.username}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, username: e.target.value }))}
|
||||
placeholder="ERP 시스템"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="discord_avatar">아바타 URL</Label>
|
||||
<Input
|
||||
id="discord_avatar"
|
||||
value={discordSettings.avatarUrl}
|
||||
onChange={(e) => setDiscordSettings((prev) => ({ ...prev, avatarUrl: e.target.value }))}
|
||||
placeholder="https://example.com/avatar.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Slack 설정 */}
|
||||
{formData.api_type === "slack" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">Slack 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="slack_webhook">웹훅 URL *</Label>
|
||||
<Input
|
||||
id="slack_webhook"
|
||||
value={slackSettings.webhookUrl}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, webhookUrl: e.target.value }))}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="slack_channel">채널</Label>
|
||||
<Input
|
||||
id="slack_channel"
|
||||
value={slackSettings.channel}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, channel: e.target.value }))}
|
||||
placeholder="#general"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="slack_username">사용자명</Label>
|
||||
<Input
|
||||
id="slack_username"
|
||||
value={slackSettings.username}
|
||||
onChange={(e) => setSlackSettings((prev) => ({ ...prev, username: e.target.value }))}
|
||||
placeholder="ERP Bot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카카오톡 설정 */}
|
||||
{formData.api_type === "kakao-talk" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">카카오톡 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="kakao_token">액세스 토큰 *</Label>
|
||||
<Input
|
||||
id="kakao_token"
|
||||
type="password"
|
||||
value={kakaoSettings.accessToken}
|
||||
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, accessToken: e.target.value }))}
|
||||
placeholder="카카오 API 액세스 토큰"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="kakao_template">템플릿 ID</Label>
|
||||
<Input
|
||||
id="kakao_template"
|
||||
value={kakaoSettings.templateId}
|
||||
onChange={(e) => setKakaoSettings((prev) => ({ ...prev, templateId: e.target.value }))}
|
||||
placeholder="template_001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 일반 API 설정 */}
|
||||
{formData.api_type === "generic" && (
|
||||
<div className="space-y-3 rounded-lg border p-4">
|
||||
<h4 className="font-medium">일반 API 설정</h4>
|
||||
<div>
|
||||
<Label htmlFor="generic_url">API URL *</Label>
|
||||
<Input
|
||||
id="generic_url"
|
||||
value={genericSettings.url}
|
||||
onChange={(e) => setGenericSettings((prev) => ({ ...prev, url: e.target.value }))}
|
||||
placeholder="https://api.example.com/webhook"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="generic_method">HTTP 메서드</Label>
|
||||
<Select
|
||||
value={genericSettings.method}
|
||||
onValueChange={(value) => setGenericSettings((prev) => ({ ...prev, method: value }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="generic_timeout">타임아웃 (ms)</Label>
|
||||
<Input
|
||||
id="generic_timeout"
|
||||
type="number"
|
||||
value={genericSettings.timeout}
|
||||
onChange={(e) => setGenericSettings((prev) => ({ ...prev, timeout: e.target.value }))}
|
||||
placeholder="30000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="generic_headers">헤더 (JSON)</Label>
|
||||
<Textarea
|
||||
id="generic_headers"
|
||||
value={genericSettings.headers}
|
||||
onChange={(e) => setGenericSettings((prev) => ({ ...prev, headers: e.target.value }))}
|
||||
placeholder='{"Content-Type": "application/json"}'
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 다른 호출 타입들 (이메일, FTP, 큐) */}
|
||||
{formData.call_type !== "rest-api" && (
|
||||
<div className="text-muted-foreground rounded-lg border p-4 text-center">
|
||||
{formData.call_type} 타입의 설정은 아직 구현되지 않았습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} disabled={loading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={loading}>
|
||||
{loading ? "저장 중..." : editingConfig ? "수정" : "생성"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
SimpleKeySettings,
|
||||
DataSaveSettings,
|
||||
ExternalCallSettings,
|
||||
SimpleExternalCallSettings,
|
||||
ConnectionSetupModalProps,
|
||||
} from "@/types/connectionTypes";
|
||||
import { isConditionalConnection } from "@/utils/connectionUtils";
|
||||
|
|
@ -20,7 +21,7 @@ import { ConditionalSettings } from "./condition/ConditionalSettings";
|
|||
import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector";
|
||||
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
|
||||
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
|
||||
import { ExternalCallSettings as ExternalCallSettingsComponent } from "./connection/ExternalCallSettings";
|
||||
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||
|
|
@ -47,12 +48,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
actions: [],
|
||||
});
|
||||
|
||||
const [externalCallSettings, setExternalCallSettings] = useState<ExternalCallSettings>({
|
||||
callType: "rest-api",
|
||||
apiUrl: "",
|
||||
httpMethod: "POST",
|
||||
headers: "{}",
|
||||
bodyTemplate: "{}",
|
||||
const [externalCallSettings, setExternalCallSettings] = useState<SimpleExternalCallSettings>({
|
||||
message: "",
|
||||
});
|
||||
|
||||
// 테이블 및 컬럼 선택을 위한 상태들
|
||||
|
|
@ -147,21 +144,21 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
}
|
||||
}
|
||||
} else if (connectionType === "external-call") {
|
||||
// 외부 호출 설정은 plan에서 로드
|
||||
const settingsRecord = settings as Record<string, unknown>;
|
||||
let externalCallData: Record<string, unknown> = {};
|
||||
|
||||
if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) {
|
||||
const planRecord = settingsRecord.plan as Record<string, unknown>;
|
||||
if (planRecord.externalCall && typeof planRecord.externalCall === "object") {
|
||||
externalCallData = planRecord.externalCall as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
setExternalCallSettings({
|
||||
callType: (settings.callType as "rest-api" | "email" | "ftp" | "queue") || "rest-api",
|
||||
apiUrl: (settings.apiUrl as string) || "",
|
||||
httpMethod: (settings.httpMethod as "GET" | "POST" | "PUT" | "DELETE") || "POST",
|
||||
headers: (settings.headers as string) || "{}",
|
||||
bodyTemplate: (settings.bodyTemplate as string) || "{}",
|
||||
// 새로운 필드들도 로드
|
||||
apiType: (settings.apiType as "slack" | "kakao-talk" | "discord" | "generic") || "generic",
|
||||
slackWebhookUrl: (settings.slackWebhookUrl as string) || "",
|
||||
slackChannel: (settings.slackChannel as string) || "",
|
||||
slackMessage: (settings.slackMessage as string) || "",
|
||||
kakaoAccessToken: (settings.kakaoAccessToken as string) || "",
|
||||
kakaoMessage: (settings.kakaoMessage as string) || "",
|
||||
discordWebhookUrl: (settings.discordWebhookUrl as string) || "",
|
||||
discordMessage: (settings.discordMessage as string) || "",
|
||||
configId: (externalCallData.configId as number) || undefined,
|
||||
configName: (externalCallData.configName as string) || undefined,
|
||||
message: (externalCallData.message as string) || "",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -338,6 +335,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
|
||||
// 연결 종류별 설정을 준비
|
||||
let settings = {};
|
||||
let plan = {}; // plan 변수 선언
|
||||
|
||||
switch (config.connectionType) {
|
||||
case "simple-key":
|
||||
|
|
@ -347,7 +345,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
settings = dataSaveSettings;
|
||||
break;
|
||||
case "external-call":
|
||||
settings = externalCallSettings;
|
||||
// 외부 호출은 plan에 저장
|
||||
plan = {
|
||||
externalCall: {
|
||||
configId: externalCallSettings.configId,
|
||||
configName: externalCallSettings.configName,
|
||||
message: externalCallSettings.message,
|
||||
},
|
||||
};
|
||||
settings = {}; // 외부 호출은 settings에 저장하지 않음
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -421,6 +427,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
settings: {
|
||||
...settings,
|
||||
...conditionalSettings, // 조건부 연결 설정 추가
|
||||
...plan, // 외부 호출 plan 추가
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -521,33 +528,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
return !hasActions || !allActionsHaveMappings || !allMappingsComplete;
|
||||
|
||||
case "external-call":
|
||||
// 외부 호출: 호출 타입과 필수 설정이 있어야 함
|
||||
const hasCallType = !!externalCallSettings.callType;
|
||||
if (!hasCallType) return true;
|
||||
|
||||
switch (externalCallSettings.callType) {
|
||||
case "rest-api":
|
||||
// REST API의 경우 apiType에 따라 다른 검증
|
||||
switch (externalCallSettings.apiType) {
|
||||
case "slack":
|
||||
return !externalCallSettings.slackWebhookUrl?.trim() || !externalCallSettings.slackMessage?.trim();
|
||||
case "kakao-talk":
|
||||
return !externalCallSettings.kakaoAccessToken?.trim() || !externalCallSettings.kakaoMessage?.trim();
|
||||
case "discord":
|
||||
return !externalCallSettings.discordWebhookUrl?.trim() || !externalCallSettings.discordMessage?.trim();
|
||||
case "generic":
|
||||
default:
|
||||
return !externalCallSettings.apiUrl?.trim();
|
||||
}
|
||||
case "email":
|
||||
return !externalCallSettings.apiUrl?.trim(); // 이메일 서버 URL 필요
|
||||
case "ftp":
|
||||
return !externalCallSettings.apiUrl?.trim(); // FTP 서버 URL 필요
|
||||
case "queue":
|
||||
return !externalCallSettings.apiUrl?.trim(); // 큐 서버 URL 필요
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
// 외부 호출: 설정 ID와 메시지가 있어야 함
|
||||
return !externalCallSettings.configId || !externalCallSettings.message?.trim();
|
||||
|
||||
default:
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalCallConfigAPI, ExternalCallConfig } from "@/lib/api/externalCallConfig";
|
||||
import { ExternalCallAPI } from "@/lib/api/externalCall";
|
||||
import { toast } from "sonner";
|
||||
import { Globe, TestTube, RefreshCw } from "lucide-react";
|
||||
|
||||
// 단순화된 외부 호출 설정 타입
|
||||
export interface SimpleExternalCallSettings {
|
||||
configId?: number; // 선택된 외부 호출 설정 ID
|
||||
configName?: string; // 설정 이름 (표시용)
|
||||
message: string; // 메시지 템플릿
|
||||
}
|
||||
|
||||
interface SimpleExternalCallSettingsProps {
|
||||
settings: SimpleExternalCallSettings;
|
||||
onSettingsChange: (settings: SimpleExternalCallSettings) => void;
|
||||
}
|
||||
|
||||
export function SimpleExternalCallSettings({ settings, onSettingsChange }: SimpleExternalCallSettingsProps) {
|
||||
const [availableConfigs, setAvailableConfigs] = useState<ExternalCallConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
|
||||
// 사용 가능한 외부 호출 설정 목록 조회
|
||||
const fetchAvailableConfigs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await ExternalCallConfigAPI.getConfigs({
|
||||
is_active: "Y", // 활성화된 설정만 조회
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setAvailableConfigs(response.data || []);
|
||||
} else {
|
||||
toast.error("외부 호출 설정 목록 조회 실패");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 목록 조회 오류:", error);
|
||||
toast.error("외부 호출 설정을 불러올 수 없습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 설정 목록 조회
|
||||
useEffect(() => {
|
||||
fetchAvailableConfigs();
|
||||
}, []);
|
||||
|
||||
// 외부 호출 설정 선택
|
||||
const handleConfigChange = (configId: string) => {
|
||||
if (configId === "none") {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
configId: undefined,
|
||||
configName: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedConfig = availableConfigs.find((config) => config.id?.toString() === configId);
|
||||
if (selectedConfig) {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
configId: selectedConfig.id,
|
||||
configName: selectedConfig.config_name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 메시지 변경
|
||||
const handleMessageChange = (message: string) => {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
// 테스트 실행
|
||||
const handleTest = async () => {
|
||||
if (!settings.configId) {
|
||||
toast.error("외부 호출 설정을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.message.trim()) {
|
||||
toast.error("메시지를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setTestLoading(true);
|
||||
|
||||
// 선택된 설정 정보 가져오기
|
||||
const configResponse = await ExternalCallConfigAPI.getConfigById(settings.configId);
|
||||
if (!configResponse.success || !configResponse.data) {
|
||||
toast.error("외부 호출 설정을 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configResponse.data;
|
||||
const configData = config.config_data as any;
|
||||
|
||||
// 백엔드 API용 설정 변환
|
||||
const backendSettings: Record<string, unknown> = {
|
||||
callType: config.call_type,
|
||||
apiType: config.api_type,
|
||||
message: settings.message,
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
// API 타입별 설정 추가
|
||||
switch (config.api_type) {
|
||||
case "discord":
|
||||
backendSettings.webhookUrl = configData.webhookUrl;
|
||||
backendSettings.username = configData.username;
|
||||
if (configData.avatarUrl) {
|
||||
backendSettings.avatarUrl = configData.avatarUrl;
|
||||
}
|
||||
break;
|
||||
|
||||
case "slack":
|
||||
backendSettings.webhookUrl = configData.webhookUrl;
|
||||
if (configData.channel) {
|
||||
backendSettings.channel = configData.channel;
|
||||
}
|
||||
if (configData.username) {
|
||||
backendSettings.username = configData.username;
|
||||
}
|
||||
break;
|
||||
|
||||
case "kakao-talk":
|
||||
backendSettings.accessToken = configData.accessToken;
|
||||
if (configData.templateId) {
|
||||
backendSettings.templateId = configData.templateId;
|
||||
}
|
||||
break;
|
||||
|
||||
case "generic":
|
||||
backendSettings.url = configData.url;
|
||||
backendSettings.method = configData.method || "POST";
|
||||
backendSettings.headers = configData.headers || {};
|
||||
if (configData.timeout) {
|
||||
backendSettings.timeout = configData.timeout;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 테스트 실행 (10초 타임아웃)
|
||||
const testPromise = ExternalCallAPI.testExternalCall({
|
||||
settings: backendSettings,
|
||||
templateData: { recordCount: 5, timestamp: new Date().toISOString() },
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("테스트가 10초 내에 완료되지 않았습니다.")), 10000),
|
||||
);
|
||||
|
||||
const result = await Promise.race([testPromise, timeoutPromise]);
|
||||
|
||||
if ((result as any).success) {
|
||||
toast.success(`테스트 성공: ${config.config_name}`);
|
||||
} else {
|
||||
toast.error(`테스트 실패: ${(result as any).message || "알 수 없는 오류"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("외부 호출 테스트 오류:", error);
|
||||
if (error instanceof Error && error.message.includes("10초")) {
|
||||
toast.error("테스트 타임아웃: 10초 내에 응답이 없습니다.");
|
||||
} else {
|
||||
toast.error("테스트 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 설정 정보
|
||||
const selectedConfig = availableConfigs.find((config) => config.id === settings.configId);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Globe size={18} />
|
||||
<h3 className="text-lg font-medium">외부 호출 설정</h3>
|
||||
</div>
|
||||
|
||||
{/* 외부 호출 설정 선택 */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label htmlFor="external-call-config">외부 호출 선택 *</Label>
|
||||
<Button variant="ghost" size="sm" onClick={fetchAvailableConfigs} disabled={loading} className="h-6 px-2">
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select value={settings.configId?.toString() || "none"} onValueChange={handleConfigChange} disabled={loading}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loading ? "로딩 중..." : "외부 호출 설정을 선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">선택 안함</SelectItem>
|
||||
{availableConfigs.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id!.toString()}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{config.config_name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{config.call_type === "rest-api" && config.api_type
|
||||
? `${config.call_type} - ${config.api_type}`
|
||||
: config.call_type}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 선택된 설정 정보 표시 */}
|
||||
{selectedConfig && (
|
||||
<div className="bg-muted/50 mt-2 rounded-lg p-3 text-sm">
|
||||
<div className="font-medium">{selectedConfig.config_name}</div>
|
||||
{selectedConfig.description && (
|
||||
<div className="text-muted-foreground mt-1">{selectedConfig.description}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 메시지 템플릿 */}
|
||||
<div>
|
||||
<Label htmlFor="external-call-message">메시지 템플릿 *</Label>
|
||||
<Textarea
|
||||
id="external-call-message"
|
||||
value={settings.message}
|
||||
onChange={(e) => handleMessageChange(e.target.value)}
|
||||
placeholder="데이터 처리가 완료되었습니다! 총 {{recordCount}}건이 처리되었습니다."
|
||||
rows={3}
|
||||
className="text-sm"
|
||||
/>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
템플릿 변수: {{ recordCount }}, {{ timestamp }}, {{ tableName }} 등을 사용할 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테스트 버튼 */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTest}
|
||||
disabled={!settings.configId || !settings.message.trim() || testLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<TestTube size={14} />
|
||||
{testLoading ? "테스트 중..." : "테스트"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 설정이 없는 경우 안내 */}
|
||||
{!loading && availableConfigs.length === 0 && (
|
||||
<div className="text-muted-foreground rounded-lg border p-4 text-center">
|
||||
<Globe size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p className="font-medium">사용 가능한 외부 호출 설정이 없습니다.</p>
|
||||
<p className="text-sm">관리자 페이지에서 외부 호출 설정을 먼저 등록해주세요.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { apiClient, ApiResponse } from "./client";
|
||||
|
||||
// 외부 호출 설정 타입 정의
|
||||
export interface ExternalCallConfig {
|
||||
id?: number;
|
||||
config_name: string;
|
||||
call_type: string;
|
||||
api_type?: string;
|
||||
config_data: Record<string, unknown>;
|
||||
description?: string;
|
||||
company_code?: string;
|
||||
is_active?: string;
|
||||
created_date?: string;
|
||||
created_by?: string;
|
||||
updated_date?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface ExternalCallConfigFilter {
|
||||
company_code?: string;
|
||||
call_type?: string;
|
||||
api_type?: string;
|
||||
is_active?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface ExternalCallConfigTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 관리 API 클라이언트
|
||||
*/
|
||||
export class ExternalCallConfigAPI {
|
||||
private static readonly BASE_URL = "/external-call-configs";
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 목록 조회
|
||||
*/
|
||||
static async getConfigs(filter?: ExternalCallConfigFilter): Promise<ApiResponse<ExternalCallConfig[]>> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filter?.company_code) params.append("company_code", filter.company_code);
|
||||
if (filter?.call_type) params.append("call_type", filter.call_type);
|
||||
if (filter?.api_type) params.append("api_type", filter.api_type);
|
||||
if (filter?.is_active) params.append("is_active", filter.is_active);
|
||||
if (filter?.search) params.append("search", filter.search);
|
||||
|
||||
const url = params.toString() ? `${this.BASE_URL}?${params.toString()}` : this.BASE_URL;
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 목록 조회 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 단일 조회
|
||||
*/
|
||||
static async getConfigById(id: number): Promise<ApiResponse<ExternalCallConfig>> {
|
||||
try {
|
||||
const response = await apiClient.get(`${this.BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`외부 호출 설정 조회 실패 (ID: ${id}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 생성
|
||||
*/
|
||||
static async createConfig(
|
||||
config: Omit<ExternalCallConfig, "id" | "created_date" | "updated_date">,
|
||||
): Promise<ApiResponse<ExternalCallConfig>> {
|
||||
try {
|
||||
const response = await apiClient.post(this.BASE_URL, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("외부 호출 설정 생성 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 수정
|
||||
*/
|
||||
static async updateConfig(id: number, config: Partial<ExternalCallConfig>): Promise<ApiResponse<ExternalCallConfig>> {
|
||||
try {
|
||||
const response = await apiClient.put(`${this.BASE_URL}/${id}`, config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`외부 호출 설정 수정 실패 (ID: ${id}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 삭제
|
||||
*/
|
||||
static async deleteConfig(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await apiClient.delete(`${this.BASE_URL}/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`외부 호출 설정 삭제 실패 (ID: ${id}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부 호출 설정 테스트
|
||||
*/
|
||||
static async testConfig(id: number): Promise<ApiResponse<ExternalCallConfigTestResult>> {
|
||||
try {
|
||||
const response = await apiClient.post(`${this.BASE_URL}/${id}/test`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`외부 호출 설정 테스트 실패 (ID: ${id}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 호출 타입 옵션
|
||||
export const CALL_TYPE_OPTIONS = [
|
||||
{ value: "rest-api", label: "REST API" },
|
||||
{ value: "email", label: "이메일" },
|
||||
{ value: "ftp", label: "FTP" },
|
||||
{ value: "queue", label: "큐" },
|
||||
];
|
||||
|
||||
// API 타입 옵션 (REST API 전용)
|
||||
export const API_TYPE_OPTIONS = [
|
||||
{ value: "discord", label: "Discord" },
|
||||
{ value: "slack", label: "Slack" },
|
||||
{ value: "kakao-talk", label: "카카오톡" },
|
||||
{ value: "mattermost", label: "Mattermost" },
|
||||
{ value: "generic", label: "기타 (일반 API)" },
|
||||
];
|
||||
|
||||
// 활성화 상태 옵션
|
||||
export const ACTIVE_STATUS_OPTIONS = [
|
||||
{ value: "Y", label: "활성" },
|
||||
{ value: "N", label: "비활성" },
|
||||
];
|
||||
|
|
@ -93,6 +93,13 @@ export interface ExternalCallSettings {
|
|||
discordUsername?: string;
|
||||
}
|
||||
|
||||
// 단순화된 외부 호출 설정 (새로운 버전)
|
||||
export interface SimpleExternalCallSettings {
|
||||
configId?: number; // 선택된 외부 호출 설정 ID
|
||||
configName?: string; // 설정 이름 (표시용)
|
||||
message: string; // 메시지 템플릿
|
||||
}
|
||||
|
||||
// ConnectionSetupModal Props 타입
|
||||
export interface ConnectionSetupModalProps {
|
||||
isOpen: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue