diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index edc62708..d35c0bcc 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -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) diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 5bb5c902..e0519334 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -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); diff --git a/backend-node/src/routes/externalCallConfigRoutes.ts b/backend-node/src/routes/externalCallConfigRoutes.ts new file mode 100644 index 00000000..394756ba --- /dev/null +++ b/backend-node/src/routes/externalCallConfigRoutes.ts @@ -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; diff --git a/backend-node/src/services/externalCallConfigService.ts b/backend-node/src/services/externalCallConfigService.ts new file mode 100644 index 00000000..ad332281 --- /dev/null +++ b/backend-node/src/services/externalCallConfigService.ts @@ -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 { + 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 { + 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 { + 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 + ): Promise { + 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 { + 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(); diff --git a/backend-node/src/types/externalCallTypes.ts b/backend-node/src/types/externalCallTypes.ts index 4df12628..47f52411 100644 --- a/backend-node/src/types/externalCallTypes.ts +++ b/backend-node/src/types/externalCallTypes.ts @@ -124,4 +124,3 @@ export type SupportedExternalCallSettings = | DiscordSettings | GenericApiSettings | EmailSettings; - diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/external-call-configs/page.tsx new file mode 100644 index 00000000..dbdd4aeb --- /dev/null +++ b/frontend/app/(main)/admin/external-call-configs/page.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [filter, setFilter] = useState({ + is_active: "Y", + }); + + // 모달 상태 + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingConfig, setEditingConfig] = useState(null); + + // 삭제 확인 다이얼로그 상태 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [configToDelete, setConfigToDelete] = useState(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 ( +
+ {/* 페이지 헤더 */} +
+
+

외부 호출 관리

+

Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.

+
+ +
+ + {/* 검색 및 필터 */} + + + + + 검색 및 필터 + + + + {/* 검색 */} +
+
+ setSearchQuery(e.target.value)} + onKeyPress={handleSearchKeyPress} + /> +
+ +
+ + {/* 필터 */} +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + {/* 설정 목록 */} + + + 외부 호출 설정 목록 + + + {loading ? ( + // 로딩 상태 +
+
로딩 중...
+
+ ) : configs.length === 0 ? ( + // 빈 상태 +
+
+ +

등록된 외부 호출 설정이 없습니다.

+

새 외부 호출을 추가해보세요.

+
+
+ ) : ( + // 설정 테이블 목록 + + + + 설정명 + 호출 타입 + API 타입 + 설명 + 상태 + 생성일 + 작업 + + + + {configs.map((config) => ( + + {config.config_name} + + {getCallTypeLabel(config.call_type)} + + + {config.api_type ? ( + {getApiTypeLabel(config.api_type)} + ) : ( + - + )} + + +
+ {config.description ? ( + + {config.description} + + ) : ( + - + )} +
+
+ + + {config.is_active === "Y" ? "활성" : "비활성"} + + + + {config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"} + + +
+ + + +
+
+
+ ))} +
+
+ )} +
+
+ + {/* 외부 호출 설정 모달 */} + setIsModalOpen(false)} + onSave={handleModalSave} + editingConfig={editingConfig} + /> + + {/* 삭제 확인 다이얼로그 */} + + + + 외부 호출 설정 삭제 + + "{configToDelete?.config_name}" 설정을 삭제하시겠습니까? +
이 작업은 되돌릴 수 없습니다. +
+
+ + 취소 + + 삭제 + + +
+
+
+ ); +} diff --git a/frontend/components/admin/ExternalCallConfigModal.tsx b/frontend/components/admin/ExternalCallConfigModal.tsx new file mode 100644 index 00000000..31aa9261 --- /dev/null +++ b/frontend/components/admin/ExternalCallConfigModal.tsx @@ -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>({ + 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 ( + + + + {editingConfig ? "외부 호출 설정 편집" : "새 외부 호출 설정"} + + +
+ {/* 기본 정보 */} +
+
+ + setFormData((prev) => ({ ...prev, config_name: e.target.value }))} + placeholder="예: 개발팀 Discord" + /> +
+ +
+ +