diff --git a/backend-node/scripts/add-data-mapping-column.js b/backend-node/scripts/add-data-mapping-column.js new file mode 100644 index 00000000..cd7ee154 --- /dev/null +++ b/backend-node/scripts/add-data-mapping-column.js @@ -0,0 +1,34 @@ +const { PrismaClient } = require("@prisma/client"); + +const prisma = new PrismaClient(); + +async function addDataMappingColumn() { + try { + console.log( + "๐Ÿ”„ external_call_configs ํ…Œ์ด๋ธ”์— data_mapping_config ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘..." + ); + + // data_mapping_config JSONB ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + await prisma.$executeRaw` + ALTER TABLE external_call_configs + ADD COLUMN IF NOT EXISTS data_mapping_config JSONB + `; + + console.log("โœ… data_mapping_config ์ปฌ๋Ÿผ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + + // ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ์— ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • + await prisma.$executeRaw` + UPDATE external_call_configs + SET data_mapping_config = '{"direction": "none"}'::jsonb + WHERE data_mapping_config IS NULL + `; + + console.log("โœ… ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ์— ๊ธฐ๋ณธ๊ฐ’์ด ์„ค์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } catch (error) { + console.error("โŒ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์‹คํŒจ:", error); + } finally { + await prisma.$disconnect(); + } +} + +addDataMappingColumn(); diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index f20c4f97..d3b53f33 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -42,6 +42,7 @@ import ddlRoutes from "./routes/ddlRoutes"; import entityReferenceRoutes from "./routes/entityReferenceRoutes"; import externalCallRoutes from "./routes/externalCallRoutes"; import externalCallConfigRoutes from "./routes/externalCallConfigRoutes"; +import dataflowExecutionRoutes from "./routes/dataflowExecutionRoutes"; import { BatchSchedulerService } from "./services/batchSchedulerService"; // import collectionRoutes from "./routes/collectionRoutes"; // ์ž„์‹œ ์ฃผ์„ // import batchRoutes from "./routes/batchRoutes"; // ์ž„์‹œ ์ฃผ์„ @@ -96,7 +97,7 @@ app.use( // Rate Limiting (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ์™„ํ™”) const limiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1๋ถ„ - max: config.nodeEnv === "development" ? 10000 : 1000, // ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋Š” 10000์œผ๋กœ ์ฆ๊ฐ€, ์šด์˜ํ™˜๊ฒฝ์—์„œ๋Š” 100 + max: config.nodeEnv === "development" ? 10000 : 100, // ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋Š” 10000์œผ๋กœ ์ฆ๊ฐ€, ์šด์˜ํ™˜๊ฒฝ์—์„œ๋Š” 100 message: { error: "๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", }, @@ -156,6 +157,7 @@ app.use("/api/ddl", ddlRoutes); app.use("/api/entity-reference", entityReferenceRoutes); app.use("/api/external-calls", externalCallRoutes); app.use("/api/external-call-configs", externalCallConfigRoutes); +app.use("/api/dataflow", dataflowExecutionRoutes); // app.use("/api/collections", collectionRoutes); // ์ž„์‹œ ์ฃผ์„ // app.use("/api/batch", batchRoutes); // ์ž„์‹œ ์ฃผ์„ // app.use('/api/users', userRoutes); diff --git a/backend-node/src/controllers/buttonDataflowController.ts b/backend-node/src/controllers/buttonDataflowController.ts index 38ca9d4c..69d623a6 100644 --- a/backend-node/src/controllers/buttonDataflowController.ts +++ b/backend-node/src/controllers/buttonDataflowController.ts @@ -727,3 +727,35 @@ function processDataflowInBackground( } }, 1000); // 1์ดˆ ํ›„ ์‹คํ–‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ } + +/** + * ๐Ÿ”ฅ ์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ (๋ฒ„ํŠผ ์ œ์–ด์šฉ) + */ +export async function getAllRelationships( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const companyCode = req.user?.companyCode || "*"; + + logger.info(`์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ - companyCode: ${companyCode}`); + + // ๋ชจ๋“  ๊ด€๊ณ„๋„์—์„œ ๊ด€๊ณ„ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ด + const allRelationships = await dataflowDiagramService.getAllRelationshipsForButtonControl(companyCode); + + logger.info(`์ „์ฒด ๊ด€๊ณ„ ${allRelationships.length}๊ฐœ ์กฐํšŒ ์™„๋ฃŒ`); + + res.json({ + success: true, + data: allRelationships, + message: `์ „์ฒด ๊ด€๊ณ„ ${allRelationships.length}๊ฐœ ์กฐํšŒ ์™„๋ฃŒ`, + }); + } catch (error) { + logger.error("์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : "์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", + errorCode: "GET_ALL_RELATIONSHIPS_ERROR", + }); + } +} diff --git a/backend-node/src/controllers/dataflowExecutionController.ts b/backend-node/src/controllers/dataflowExecutionController.ts new file mode 100644 index 00000000..68ae2507 --- /dev/null +++ b/backend-node/src/controllers/dataflowExecutionController.ts @@ -0,0 +1,235 @@ +/** + * ๐Ÿ”ฅ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ ์ปจํŠธ๋กค๋Ÿฌ + * + * ๋ฒ„ํŠผ ์ œ์–ด์—์„œ ๊ด€๊ณ„ ์‹คํ–‰ ์‹œ ์‚ฌ์šฉ๋˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ + */ + +import { Request, Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; +import { PrismaClient } from "@prisma/client"; +import logger from "../utils/logger"; + +const prisma = new PrismaClient(); + +/** + * ๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ + */ +export async function executeDataAction( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, data, actionType, connection } = req.body; + const companyCode = req.user?.companyCode || "*"; + + logger.info(`๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ ์‹œ์ž‘: ${actionType} on ${tableName}`, { + tableName, + actionType, + dataKeys: Object.keys(data), + connection: connection?.name, + }); + + // ์—ฐ๊ฒฐ ์ •๋ณด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ + let result; + + if (connection && connection.id !== 0) { + // ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ + result = await executeExternalDatabaseAction(tableName, data, actionType, connection); + } else { + // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (ํ˜„์žฌ ์‹œ์Šคํ…œ) + result = await executeMainDatabaseAction(tableName, data, actionType, companyCode); + } + + logger.info(`๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ ์™„๋ฃŒ: ${actionType} on ${tableName}`, result); + + res.json({ + success: true, + message: `๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ ์™„๋ฃŒ: ${actionType}`, + data: result, + }); + + } catch (error: any) { + logger.error("๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: `๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ ์‹คํŒจ: ${error.message}`, + errorCode: "DATA_ACTION_EXECUTION_ERROR", + }); + } +} + +/** + * ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ + */ +async function executeMainDatabaseAction( + tableName: string, + data: Record, + actionType: string, + companyCode: string +): Promise { + try { + // ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”๊ฐ€ + const dataWithCompany = { + ...data, + company_code: companyCode, + }; + + switch (actionType.toLowerCase()) { + case 'insert': + return await executeInsert(tableName, dataWithCompany); + case 'update': + return await executeUpdate(tableName, dataWithCompany); + case 'upsert': + return await executeUpsert(tableName, dataWithCompany); + case 'delete': + return await executeDelete(tableName, dataWithCompany); + default: + throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…: ${actionType}`); + } + } catch (error) { + logger.error(`๋ฉ”์ธ DB ์•ก์…˜ ์‹คํ–‰ ์˜ค๋ฅ˜ (${actionType}):`, error); + throw error; + } +} + +/** + * ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ + */ +async function executeExternalDatabaseAction( + tableName: string, + data: Record, + actionType: string, + connection: any +): Promise { + try { + // TODO: ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ์‹คํ–‰ ๋กœ์ง ๊ตฌํ˜„ + // ํ˜„์žฌ๋Š” ๋กœ๊ทธ๋งŒ ์ถœ๋ ฅํ•˜๊ณ  ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + logger.info(`์™ธ๋ถ€ DB ์•ก์…˜ ์‹คํ–‰: ${connection.name} (${connection.host}:${connection.port})`); + logger.info(`ํ…Œ์ด๋ธ”: ${tableName}, ์•ก์…˜: ${actionType}`, data); + + // ์ž„์‹œ ์„ฑ๊ณต ์‘๋‹ต + return { + success: true, + message: `์™ธ๋ถ€ DB ์•ก์…˜ ์‹คํ–‰ ์™„๋ฃŒ: ${actionType} on ${tableName}`, + connection: connection.name, + affectedRows: 1, + }; + } catch (error) { + logger.error(`์™ธ๋ถ€ DB ์•ก์…˜ ์‹คํ–‰ ์˜ค๋ฅ˜ (${actionType}):`, error); + throw error; + } +} + +/** + * INSERT ์‹คํ–‰ + */ +async function executeInsert(tableName: string, data: Record): Promise { + try { + // ๋™์  ํ…Œ์ด๋ธ” ์ ‘๊ทผ์„ ์œ„ํ•œ raw query ์‚ฌ์šฉ + const columns = Object.keys(data).join(', '); + const values = Object.values(data); + const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); + + const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`; + + logger.info(`INSERT ์ฟผ๋ฆฌ ์‹คํ–‰:`, { query, values }); + + const result = await prisma.$queryRawUnsafe(query, ...values); + + return { + success: true, + action: 'insert', + tableName, + data: result, + affectedRows: Array.isArray(result) ? result.length : 1, + }; + } catch (error) { + logger.error(`INSERT ์‹คํ–‰ ์˜ค๋ฅ˜:`, error); + throw error; + } +} + +/** + * UPDATE ์‹คํ–‰ + */ +async function executeUpdate(tableName: string, data: Record): Promise { + try { + // ID ๋˜๋Š” ๊ธฐ๋ณธํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์—…๋ฐ์ดํŠธ + const { id, ...updateData } = data; + + if (!id) { + throw new Error('UPDATE๋ฅผ ์œ„ํ•œ ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'); + } + + const setClause = Object.keys(updateData) + .map((key, index) => `${key} = $${index + 1}`) + .join(', '); + + const values = Object.values(updateData); + const query = `UPDATE ${tableName} SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`; + + logger.info(`UPDATE ์ฟผ๋ฆฌ ์‹คํ–‰:`, { query, values: [...values, id] }); + + const result = await prisma.$queryRawUnsafe(query, ...values, id); + + return { + success: true, + action: 'update', + tableName, + data: result, + affectedRows: Array.isArray(result) ? result.length : 1, + }; + } catch (error) { + logger.error(`UPDATE ์‹คํ–‰ ์˜ค๋ฅ˜:`, error); + throw error; + } +} + +/** + * UPSERT ์‹คํ–‰ + */ +async function executeUpsert(tableName: string, data: Record): Promise { + try { + // ๋จผ์ € INSERT๋ฅผ ์‹œ๋„ํ•˜๊ณ , ์‹คํŒจํ•˜๋ฉด UPDATE + try { + return await executeInsert(tableName, data); + } catch (insertError) { + // INSERT ์‹คํŒจ ์‹œ UPDATE ์‹œ๋„ + logger.info(`INSERT ์‹คํŒจ, UPDATE ์‹œ๋„:`, insertError); + return await executeUpdate(tableName, data); + } + } catch (error) { + logger.error(`UPSERT ์‹คํ–‰ ์˜ค๋ฅ˜:`, error); + throw error; + } +} + +/** + * DELETE ์‹คํ–‰ + */ +async function executeDelete(tableName: string, data: Record): Promise { + try { + const { id } = data; + + if (!id) { + throw new Error('DELETE๋ฅผ ์œ„ํ•œ ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค'); + } + + const query = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`; + + logger.info(`DELETE ์ฟผ๋ฆฌ ์‹คํ–‰:`, { query, values: [id] }); + + const result = await prisma.$queryRawUnsafe(query, id); + + return { + success: true, + action: 'delete', + tableName, + data: result, + affectedRows: Array.isArray(result) ? result.length : 1, + }; + } catch (error) { + logger.error(`DELETE ์‹คํ–‰ ์˜ค๋ฅ˜:`, error); + throw error; + } +} diff --git a/backend-node/src/routes/dataflowExecutionRoutes.ts b/backend-node/src/routes/dataflowExecutionRoutes.ts new file mode 100644 index 00000000..9271defe --- /dev/null +++ b/backend-node/src/routes/dataflowExecutionRoutes.ts @@ -0,0 +1,19 @@ +/** + * ๐Ÿ”ฅ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ ๋ผ์šฐํŠธ + * + * ๋ฒ„ํŠผ ์ œ์–ด์—์„œ ๊ด€๊ณ„ ์‹คํ–‰ ์‹œ ์‚ฌ์šฉ๋˜๋Š” API ์—”๋“œํฌ์ธํŠธ + */ + +import express from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { executeDataAction } from "../controllers/dataflowExecutionController"; + +const router = express.Router(); + +// ๐Ÿ”ฅ ๋ชจ๋“  ๋ผ์šฐํŠธ์— ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ์ ์šฉ +router.use(authenticateToken); + +// ๋ฐ์ดํ„ฐ ์•ก์…˜ ์‹คํ–‰ +router.post("/execute-data-action", executeDataAction); + +export default router; diff --git a/backend-node/src/routes/externalCallConfigRoutes.ts b/backend-node/src/routes/externalCallConfigRoutes.ts index 394756ba..5cd56969 100644 --- a/backend-node/src/routes/externalCallConfigRoutes.ts +++ b/backend-node/src/routes/externalCallConfigRoutes.ts @@ -249,4 +249,80 @@ router.post("/:id/test", async (req: Request, res: Response) => { } }); +/** + * ๐Ÿ”ฅ ๊ฐœ์„ ๋œ ์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ (๋ฐ์ดํ„ฐ ๋งคํ•‘ ํ†ตํ•ฉ) + * POST /api/external-call-configs/:id/execute + */ +router.post("/:id/execute", 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 { requestData, contextData } = req.body; + + // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + const userInfo = (req as any).user; + const userId = userInfo?.userId || "SYSTEM"; + const companyCode = userInfo?.companyCode || "*"; + + const executionResult = await externalCallConfigService.executeConfigWithDataMapping( + id, + requestData || {}, + { + ...contextData, + userId, + companyCode, + executedAt: new Date().toISOString(), + } + ); + + return res.json({ + success: executionResult.success, + message: executionResult.message, + data: executionResult.data, + executionTime: executionResult.executionTime, + error: executionResult.error, + }); + } catch (error) { + logger.error("์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ API ์˜ค๋ฅ˜:", error); + return res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : "์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹คํŒจ", + errorCode: "EXTERNAL_CALL_EXECUTE_ERROR", + }); + } +}); + +/** + * ๐Ÿ”ฅ ๋ฒ„ํŠผ ์ œ์–ด์šฉ ์™ธ๋ถ€ํ˜ธ์ถœ ๋ชฉ๋ก ์กฐํšŒ (๊ฐ„์†Œํ™”๋œ ์ •๋ณด) + * GET /api/external-call-configs/for-button-control + */ +router.get("/for-button-control", async (req: Request, res: Response) => { + try { + const userInfo = (req as any).user; + const companyCode = userInfo?.companyCode || "*"; + + const configs = await externalCallConfigService.getConfigsForButtonControl(companyCode); + + 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_BUTTON_CONTROL_LIST_ERROR", + }); + } +}); + export default router; diff --git a/backend-node/src/routes/testButtonDataflowRoutes.ts b/backend-node/src/routes/testButtonDataflowRoutes.ts index bfe61ab0..e7b3b83c 100644 --- a/backend-node/src/routes/testButtonDataflowRoutes.ts +++ b/backend-node/src/routes/testButtonDataflowRoutes.ts @@ -14,6 +14,7 @@ import { executeOptimizedButton, executeSimpleDataflow, getJobStatus, + getAllRelationships, } from "../controllers/buttonDataflowController"; import { AuthenticatedRequest } from "../types/auth"; import config from "../config/environment"; @@ -52,6 +53,9 @@ if (config.nodeEnv !== "production") { // ํŠน์ • ๊ด€๊ณ„๋„์˜ ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ router.get("/diagrams/:diagramId/relationships", getDiagramRelationships); + // ๐Ÿ”ฅ ์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ (๋ฒ„ํŠผ ์ œ์–ด์šฉ) + router.get("/relationships/all", getAllRelationships); + // ๊ด€๊ณ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ •๋ณด ์กฐํšŒ router.get( "/diagrams/:diagramId/relationships/:relationshipId/preview", diff --git a/backend-node/src/services/dataMappingService.ts b/backend-node/src/services/dataMappingService.ts new file mode 100644 index 00000000..af7e9759 --- /dev/null +++ b/backend-node/src/services/dataMappingService.ts @@ -0,0 +1,575 @@ +import { PrismaClient } from "@prisma/client"; +import { + DataMappingConfig, + InboundMapping, + OutboundMapping, + FieldMapping, + DataMappingResult, + MappingValidationResult, + FieldTransform, + DataType, +} from "../types/dataMappingTypes"; + +export class DataMappingService { + private prisma: PrismaClient; + + constructor() { + this.prisma = new PrismaClient(); + } + + /** + * Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ (์™ธ๋ถ€ โ†’ ๋‚ด๋ถ€) + */ + async processInboundData( + externalData: any, + mapping: InboundMapping + ): Promise { + const startTime = Date.now(); + const result: DataMappingResult = { + success: false, + direction: "inbound", + recordsProcessed: 0, + recordsInserted: 0, + recordsUpdated: 0, + recordsSkipped: 0, + errors: [], + executionTime: 0, + timestamp: new Date().toISOString(), + }; + + try { + console.log(`๐Ÿ“ฅ [DataMappingService] Inbound ๋งคํ•‘ ์‹œ์ž‘:`, { + targetTable: mapping.targetTable, + insertMode: mapping.insertMode, + fieldMappings: mapping.fieldMappings.length, + }); + + // ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ + const dataArray = Array.isArray(externalData) + ? externalData + : [externalData]; + result.recordsProcessed = dataArray.length; + + // ๊ฐ ๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ + for (const record of dataArray) { + try { + const mappedData = await this.mapInboundRecord(record, mapping); + + if (Object.keys(mappedData).length === 0) { + result.recordsSkipped!++; + continue; + } + + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ + await this.saveInboundRecord(mappedData, mapping); + + if (mapping.insertMode === "insert") { + result.recordsInserted!++; + } else { + result.recordsUpdated!++; + } + } catch (error) { + console.error(`โŒ [DataMappingService] ๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ ์‹คํŒจ:`, error); + result.errors!.push( + `๋ ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ ์‹คํŒจ: ${error instanceof Error ? error.message : String(error)}` + ); + result.recordsSkipped!++; + } + } + + result.success = + result.errors!.length === 0 || + result.recordsInserted! > 0 || + result.recordsUpdated! > 0; + } catch (error) { + console.error(`โŒ [DataMappingService] Inbound ๋งคํ•‘ ์‹คํŒจ:`, error); + result.errors!.push( + `๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹คํŒจ: ${error instanceof Error ? error.message : String(error)}` + ); + } + + result.executionTime = Date.now() - startTime; + + console.log(`โœ… [DataMappingService] Inbound ๋งคํ•‘ ์™„๋ฃŒ:`, result); + return result; + } + + /** + * Outbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ (๋‚ด๋ถ€ โ†’ ์™ธ๋ถ€) + */ + async processOutboundData( + mapping: OutboundMapping, + filter?: any + ): Promise { + console.log(`๐Ÿ“ค [DataMappingService] Outbound ๋งคํ•‘ ์‹œ์ž‘:`, { + sourceTable: mapping.sourceTable, + fieldMappings: mapping.fieldMappings.length, + filter, + }); + + try { + // ์†Œ์Šค ๋ฐ์ดํ„ฐ ์กฐํšŒ + const sourceData = await this.getSourceData(mapping, filter); + + if ( + !sourceData || + (Array.isArray(sourceData) && sourceData.length === 0) + ) { + console.log(`โš ๏ธ [DataMappingService] ์†Œ์Šค ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`); + return null; + } + + // ๋ฐ์ดํ„ฐ ๋งคํ•‘ + const mappedData = Array.isArray(sourceData) + ? await Promise.all( + sourceData.map((record) => this.mapOutboundRecord(record, mapping)) + ) + : await this.mapOutboundRecord(sourceData, mapping); + + console.log(`โœ… [DataMappingService] Outbound ๋งคํ•‘ ์™„๋ฃŒ:`, { + recordCount: Array.isArray(mappedData) ? mappedData.length : 1, + }); + + return mappedData; + } catch (error) { + console.error(`โŒ [DataMappingService] Outbound ๋งคํ•‘ ์‹คํŒจ:`, error); + throw error; + } + } + + /** + * ๋‹จ์ผ Inbound ๋ ˆ์ฝ”๋“œ ๋งคํ•‘ + */ + private async mapInboundRecord( + sourceRecord: any, + mapping: InboundMapping + ): Promise> { + const mappedRecord: Record = {}; + + for (const fieldMapping of mapping.fieldMappings) { + try { + const sourceValue = sourceRecord[fieldMapping.sourceField]; + + // ํ•„์ˆ˜ ํ•„๋“œ ์ฒดํฌ + if ( + fieldMapping.required && + (sourceValue === undefined || sourceValue === null) + ) { + if (fieldMapping.defaultValue !== undefined) { + mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue; + } else { + throw new Error( + `ํ•„์ˆ˜ ํ•„๋“œ '${fieldMapping.sourceField}'๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.` + ); + } + continue; + } + + // ๊ฐ’์ด ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ + if (sourceValue === undefined || sourceValue === null) { + if (fieldMapping.defaultValue !== undefined) { + mappedRecord[fieldMapping.targetField] = fieldMapping.defaultValue; + } + continue; + } + + // ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ์ ์šฉ + const transformedValue = await this.transformFieldValue( + sourceValue, + fieldMapping.dataType, + fieldMapping.transform + ); + + mappedRecord[fieldMapping.targetField] = transformedValue; + } catch (error) { + console.error( + `โŒ [DataMappingService] ํ•„๋“œ ๋งคํ•‘ ์‹คํŒจ (${fieldMapping.sourceField} โ†’ ${fieldMapping.targetField}):`, + error + ); + throw error; + } + } + + return mappedRecord; + } + + /** + * ๋‹จ์ผ Outbound ๋ ˆ์ฝ”๋“œ ๋งคํ•‘ + */ + private async mapOutboundRecord( + sourceRecord: any, + mapping: OutboundMapping + ): Promise> { + const mappedRecord: Record = {}; + + for (const fieldMapping of mapping.fieldMappings) { + try { + const sourceValue = sourceRecord[fieldMapping.sourceField]; + + // ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ์ ์šฉ + const transformedValue = await this.transformFieldValue( + sourceValue, + fieldMapping.dataType, + fieldMapping.transform + ); + + mappedRecord[fieldMapping.targetField] = transformedValue; + } catch (error) { + console.error( + `โŒ [DataMappingService] ํ•„๋“œ ๋งคํ•‘ ์‹คํŒจ (${fieldMapping.sourceField} โ†’ ${fieldMapping.targetField}):`, + error + ); + throw error; + } + } + + return mappedRecord; + } + + /** + * ํ•„๋“œ ๊ฐ’ ๋ณ€ํ™˜ + */ + private async transformFieldValue( + value: any, + targetDataType: DataType, + transform?: FieldTransform + ): Promise { + let transformedValue = value; + + // 1. ๋ณ€ํ™˜ ํ•จ์ˆ˜ ์ ์šฉ + if (transform) { + switch (transform.type) { + case "constant": + transformedValue = transform.value; + break; + + case "format": + if (targetDataType === "date" && transform.format) { + transformedValue = this.formatDate(value, transform.format); + } + break; + + case "function": + if (transform.functionName) { + transformedValue = await this.applyCustomFunction( + value, + transform.functionName + ); + } + break; + } + } + + // 2. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜ + return this.convertDataType(transformedValue, targetDataType); + } + + /** + * ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜ + */ + private convertDataType(value: any, targetType: DataType): any { + if (value === null || value === undefined) return value; + + switch (targetType) { + case "string": + return String(value); + case "number": + const num = Number(value); + return isNaN(num) ? null : num; + case "boolean": + if (typeof value === "boolean") return value; + if (typeof value === "string") { + return ( + value.toLowerCase() === "true" || value === "1" || value === "Y" + ); + } + return Boolean(value); + case "date": + return new Date(value); + case "json": + return typeof value === "string" ? JSON.parse(value) : value; + default: + return value; + } + } + + /** + * ๋‚ ์งœ ํฌ๋งท ๋ณ€ํ™˜ + */ + private formatDate(value: any, format: string): string { + const date = new Date(value); + if (isNaN(date.getTime())) return value; + + // ๊ฐ„๋‹จํ•œ ๋‚ ์งœ ํฌ๋งท ๋ณ€ํ™˜ + switch (format) { + case "YYYY-MM-DD": + return date.toISOString().split("T")[0]; + case "YYYY-MM-DD HH:mm:ss": + return date + .toISOString() + .replace("T", " ") + .replace(/\.\d{3}Z$/, ""); + default: + return date.toISOString(); + } + } + + /** + * ์ปค์Šคํ…€ ํ•จ์ˆ˜ ์ ์šฉ + */ + private async applyCustomFunction( + value: any, + functionName: string + ): Promise { + // ์ถ”ํ›„ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์ปค์Šคํ…€ ํ•จ์ˆ˜๋“ค + switch (functionName) { + case "upperCase": + return String(value).toUpperCase(); + case "lowerCase": + return String(value).toLowerCase(); + case "trim": + return String(value).trim(); + default: + console.warn( + `โš ๏ธ [DataMappingService] ์•Œ ์ˆ˜ ์—†๋Š” ํ•จ์ˆ˜: ${functionName}` + ); + return value; + } + } + + /** + * Inbound ๋ฐ์ดํ„ฐ ์ €์žฅ + */ + private async saveInboundRecord( + mappedData: Record, + mapping: InboundMapping + ): Promise { + const tableName = mapping.targetTable; + + try { + switch (mapping.insertMode) { + case "insert": + await this.executeInsert(tableName, mappedData); + break; + + case "upsert": + await this.executeUpsert( + tableName, + mappedData, + mapping.keyFields || [] + ); + break; + + case "update": + await this.executeUpdate( + tableName, + mappedData, + mapping.keyFields || [] + ); + break; + } + } catch (error) { + console.error( + `โŒ [DataMappingService] ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹คํŒจ (${tableName}):`, + error + ); + throw error; + } + } + + /** + * ์†Œ์Šค ๋ฐ์ดํ„ฐ ์กฐํšŒ + */ + private async getSourceData( + mapping: OutboundMapping, + filter?: any + ): Promise { + const tableName = mapping.sourceTable; + + try { + // ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ (Prisma์˜ ๊ฒฝ์šฐ ๋Ÿฐํƒ€์ž„์—์„œ ์ œํ•œ์ ) + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๊ฐ ํ…Œ์ด๋ธ”๋ณ„ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ Raw SQL์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ + + let whereClause = {}; + if (mapping.sourceFilter) { + // ๊ฐ„๋‹จํ•œ ํ•„ํ„ฐ ํŒŒ์‹ฑ (์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ํŒŒ์‹ฑ ํ•„์š”) + console.log( + `๐Ÿ” [DataMappingService] ํ•„ํ„ฐ ์กฐ๊ฑด: ${mapping.sourceFilter}` + ); + // TODO: ํ•„ํ„ฐ ์กฐ๊ฑด ํŒŒ์‹ฑ ๋ฐ ์ ์šฉ + } + + if (filter) { + whereClause = { ...whereClause, ...filter }; + } + + // Raw SQL์„ ์‚ฌ์šฉํ•œ ๋™์  ์ฟผ๋ฆฌ + const query = `SELECT * FROM ${tableName}${mapping.sourceFilter ? ` WHERE ${mapping.sourceFilter}` : ""}`; + console.log(`๐Ÿ” [DataMappingService] ์ฟผ๋ฆฌ ์‹คํ–‰: ${query}`); + + const result = await this.prisma.$queryRawUnsafe(query); + return result; + } catch (error) { + console.error( + `โŒ [DataMappingService] ์†Œ์Šค ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ (${tableName}):`, + error + ); + throw error; + } + } + + /** + * INSERT ์‹คํ–‰ + */ + private async executeInsert( + tableName: string, + data: Record + ): Promise { + const columns = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, i) => `$${i + 1}`).join(", "); + + const query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`; + + console.log(`๐Ÿ“ [DataMappingService] INSERT ์‹คํ–‰:`, { + table: tableName, + columns, + query, + }); + await this.prisma.$executeRawUnsafe(query, ...values); + } + + /** + * UPSERT ์‹คํ–‰ + */ + private async executeUpsert( + tableName: string, + data: Record, + keyFields: string[] + ): Promise { + if (keyFields.length === 0) { + throw new Error("UPSERT ๋ชจ๋“œ์—์„œ๋Š” ํ‚ค ํ•„๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + + const columns = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, i) => `$${i + 1}`).join(", "); + + const updateClauses = columns + .filter((col) => !keyFields.includes(col)) + .map((col) => `${col} = EXCLUDED.${col}`) + .join(", "); + + const query = ` + INSERT INTO ${tableName} (${columns.join(", ")}) + VALUES (${placeholders}) + ON CONFLICT (${keyFields.join(", ")}) + DO UPDATE SET ${updateClauses} + `; + + console.log(`๐Ÿ”„ [DataMappingService] UPSERT ์‹คํ–‰:`, { + table: tableName, + keyFields, + query, + }); + await this.prisma.$executeRawUnsafe(query, ...values); + } + + /** + * UPDATE ์‹คํ–‰ + */ + private async executeUpdate( + tableName: string, + data: Record, + keyFields: string[] + ): Promise { + if (keyFields.length === 0) { + throw new Error("UPDATE ๋ชจ๋“œ์—์„œ๋Š” ํ‚ค ํ•„๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + + const updateColumns = Object.keys(data).filter( + (col) => !keyFields.includes(col) + ); + const updateClauses = updateColumns + .map((col, i) => `${col} = $${i + 1}`) + .join(", "); + + const whereConditions = keyFields + .map((field, i) => `${field} = $${updateColumns.length + i + 1}`) + .join(" AND "); + + const values = [ + ...updateColumns.map((col) => data[col]), + ...keyFields.map((field) => data[field]), + ]; + + const query = `UPDATE ${tableName} SET ${updateClauses} WHERE ${whereConditions}`; + + console.log(`โœ๏ธ [DataMappingService] UPDATE ์‹คํ–‰:`, { + table: tableName, + keyFields, + query, + }); + await this.prisma.$executeRawUnsafe(query, ...values); + } + + /** + * ๋งคํ•‘ ์„ค์ • ๊ฒ€์ฆ + */ + validateMappingConfig(config: DataMappingConfig): MappingValidationResult { + const result: MappingValidationResult = { + isValid: true, + errors: [], + warnings: [], + }; + + if (config.direction === "none") { + return result; + } + + // Inbound ๋งคํ•‘ ๊ฒ€์ฆ + if ( + (config.direction === "inbound" || + config.direction === "bidirectional") && + config.inboundMapping + ) { + if (!config.inboundMapping.targetTable) { + result.errors.push("Inbound ๋งคํ•‘์— ๋Œ€์ƒ ํ…Œ์ด๋ธ”์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + if (config.inboundMapping.fieldMappings.length === 0) { + result.errors.push("Inbound ๋งคํ•‘์— ํ•„๋“œ ๋งคํ•‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + if ( + config.inboundMapping.insertMode !== "insert" && + (!config.inboundMapping.keyFields || + config.inboundMapping.keyFields.length === 0) + ) { + result.errors.push("UPSERT/UPDATE ๋ชจ๋“œ์—์„œ๋Š” ํ‚ค ํ•„๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + } + + // Outbound ๋งคํ•‘ ๊ฒ€์ฆ + if ( + (config.direction === "outbound" || + config.direction === "bidirectional") && + config.outboundMapping + ) { + if (!config.outboundMapping.sourceTable) { + result.errors.push("Outbound ๋งคํ•‘์— ์†Œ์Šค ํ…Œ์ด๋ธ”์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + if (config.outboundMapping.fieldMappings.length === 0) { + result.errors.push("Outbound ๋งคํ•‘์— ํ•„๋“œ ๋งคํ•‘์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); + } + } + + result.isValid = result.errors.length === 0; + return result; + } + + /** + * ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + */ + async disconnect(): Promise { + await this.prisma.$disconnect(); + } +} diff --git a/backend-node/src/services/dataflowDiagramService.ts b/backend-node/src/services/dataflowDiagramService.ts index c391ef4e..427b60c5 100644 --- a/backend-node/src/services/dataflowDiagramService.ts +++ b/backend-node/src/services/dataflowDiagramService.ts @@ -384,3 +384,66 @@ export const copyDataflowDiagram = async ( throw error; } }; + +/** + * ๐Ÿ”ฅ ์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ (๋ฒ„ํŠผ ์ œ์–ด์šฉ) + * dataflow_diagrams ํ…Œ์ด๋ธ”์—์„œ ๊ด€๊ณ„๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒ (๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ด€๊ณ„ ํ™”๋ฉด๊ณผ ๋™์ผ) + */ +export const getAllRelationshipsForButtonControl = async ( + companyCode: string +): Promise> => { + try { + logger.info(`์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์‹œ์ž‘ - companyCode: ${companyCode}`); + + // dataflow_diagrams ํ…Œ์ด๋ธ”์—์„œ ๊ด€๊ณ„๋„๋“ค์„ ์กฐํšŒ + const diagrams = await prisma.dataflow_diagrams.findMany({ + where: { + company_code: companyCode, + }, + select: { + diagram_id: true, + diagram_name: true, + relationships: true, + }, + orderBy: { + updated_at: "desc", + }, + }); + + const allRelationships = diagrams.map((diagram) => { + // relationships ๊ตฌ์กฐ์—์„œ ํ…Œ์ด๋ธ” ์ •๋ณด ์ถ”์ถœ + const relationships = diagram.relationships as any || {}; + + // ํ…Œ์ด๋ธ” ์ •๋ณด ์ถ”์ถœ + let sourceTable = ""; + let targetTable = ""; + + if (relationships.fromTable?.tableName) { + sourceTable = relationships.fromTable.tableName; + } + if (relationships.toTable?.tableName) { + targetTable = relationships.toTable.tableName; + } + + return { + id: diagram.diagram_id.toString(), + name: diagram.diagram_name || `๊ด€๊ณ„ ${diagram.diagram_id}`, + sourceTable: sourceTable, + targetTable: targetTable, + category: "๋ฐ์ดํ„ฐ ํ๋ฆ„", + }; + }); + + logger.info(`์ „์ฒด ๊ด€๊ณ„ ${allRelationships.length}๊ฐœ ์กฐํšŒ ์™„๋ฃŒ`); + return allRelationships; + } catch (error) { + logger.error("์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์„œ๋น„์Šค ์˜ค๋ฅ˜:", error); + throw error; + } +}; diff --git a/backend-node/src/services/externalCallConfigService.ts b/backend-node/src/services/externalCallConfigService.ts index ad332281..2ad6d629 100644 --- a/backend-node/src/services/externalCallConfigService.ts +++ b/backend-node/src/services/externalCallConfigService.ts @@ -308,6 +308,265 @@ export class ExternalCallConfigService { }; } } + + /** + * ๐Ÿ”ฅ ๋ฐ์ดํ„ฐ ๋งคํ•‘๊ณผ ํ•จ๊ป˜ ์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ + */ + async executeConfigWithDataMapping( + configId: number, + requestData: Record, + contextData: Record + ): Promise<{ + success: boolean; + message: string; + data?: any; + executionTime: number; + error?: string; + }> { + const startTime = performance.now(); + + try { + logger.info(`=== ์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹œ์ž‘ (ID: ${configId}) ===`); + + // 1. ์„ค์ • ์กฐํšŒ + const config = await this.getConfigById(configId); + if (!config) { + throw new Error(`์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ •์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${configId}`); + } + + // 2. ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ (์žˆ๋Š” ๊ฒฝ์šฐ) + let processedData = requestData; + const configData = config.config_data as any; + if (configData?.dataMappingConfig?.outboundMapping) { + logger.info("Outbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์ค‘..."); + processedData = await this.processOutboundMapping( + configData.dataMappingConfig.outboundMapping, + requestData + ); + } + + // 3. ์™ธ๋ถ€ API ํ˜ธ์ถœ + const callResult = await this.executeExternalCall(config, processedData, contextData); + + // 4. Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ (์žˆ๋Š” ๊ฒฝ์šฐ) + if ( + callResult.success && + configData?.dataMappingConfig?.inboundMapping + ) { + logger.info("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์ค‘..."); + await this.processInboundMapping( + configData.dataMappingConfig.inboundMapping, + callResult.data + ); + } + + const executionTime = performance.now() - startTime; + logger.info(`์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์™„๋ฃŒ: ${executionTime.toFixed(2)}ms`); + + return { + success: callResult.success, + message: callResult.success + ? `์™ธ๋ถ€ํ˜ธ์ถœ '${config.config_name}' ์‹คํ–‰ ์™„๋ฃŒ` + : `์™ธ๋ถ€ํ˜ธ์ถœ '${config.config_name}' ์‹คํ–‰ ์‹คํŒจ`, + data: callResult.data, + executionTime, + error: callResult.error, + }; + } catch (error) { + const executionTime = performance.now() - startTime; + logger.error("์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹คํŒจ:", error); + + const errorMessage = error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; + + return { + success: false, + message: `์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹คํŒจ: ${errorMessage}`, + executionTime, + error: errorMessage, + }; + } + } + + /** + * ๐Ÿ”ฅ ๋ฒ„ํŠผ ์ œ์–ด์šฉ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ๋ชฉ๋ก ์กฐํšŒ (๊ฐ„์†Œํ™”๋œ ์ •๋ณด) + */ + async getConfigsForButtonControl(companyCode: string): Promise> { + try { + const configs = await prisma.external_call_configs.findMany({ + where: { + company_code: companyCode, + is_active: "Y", + }, + select: { + id: true, + config_name: true, + description: true, + config_data: true, + }, + orderBy: { + config_name: "asc", + }, + }); + + return configs.map((config) => { + const configData = config.config_data as any; + return { + id: config.id.toString(), + name: config.config_name, + description: config.description || undefined, + apiUrl: configData?.restApiSettings?.apiUrl || "", + method: configData?.restApiSettings?.httpMethod || "GET", + hasDataMapping: !!(configData?.dataMappingConfig), + }; + }); + } catch (error) { + logger.error("๋ฒ„ํŠผ ์ œ์–ด์šฉ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์กฐํšŒ ์‹คํŒจ:", error); + throw error; + } + } + + /** + * ๐Ÿ”ฅ ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹คํ–‰ + */ + private async executeExternalCall( + config: ExternalCallConfig, + requestData: Record, + contextData: Record + ): Promise<{ success: boolean; data?: any; error?: string }> { + try { + const configData = config.config_data as any; + const restApiSettings = configData?.restApiSettings; + if (!restApiSettings) { + throw new Error("REST API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค."); + } + + const { apiUrl, httpMethod, headers = {}, timeout = 30000 } = restApiSettings; + + // ์š”์ฒญ ํ—ค๋” ์ค€๋น„ + const requestHeaders = { + "Content-Type": "application/json", + ...headers, + }; + + // ์ธ์ฆ ์ฒ˜๋ฆฌ + if (restApiSettings.authentication?.type === "basic") { + const { username, password } = restApiSettings.authentication; + const credentials = Buffer.from(`${username}:${password}`).toString("base64"); + requestHeaders["Authorization"] = `Basic ${credentials}`; + } else if (restApiSettings.authentication?.type === "bearer") { + const { token } = restApiSettings.authentication; + requestHeaders["Authorization"] = `Bearer ${token}`; + } + + // ์š”์ฒญ ๋ณธ๋ฌธ ์ค€๋น„ + let requestBody = undefined; + if (["POST", "PUT", "PATCH"].includes(httpMethod.toUpperCase())) { + requestBody = JSON.stringify({ + ...requestData, + _context: contextData, // ์ปจํ…์ŠคํŠธ ์ •๋ณด ์ถ”๊ฐ€ + }); + } + + logger.info(`์™ธ๋ถ€ API ํ˜ธ์ถœ: ${httpMethod} ${apiUrl}`); + + // ์‹ค์ œ HTTP ์š”์ฒญ (์—ฌ๊ธฐ์„œ๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ) + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” axios๋‚˜ fetch๋ฅผ ์‚ฌ์šฉ + const response = await fetch(apiUrl, { + method: httpMethod, + headers: requestHeaders, + body: requestBody, + signal: AbortSignal.timeout(timeout), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const responseData = await response.json(); + + return { + success: true, + data: responseData, + }; + } catch (error) { + logger.error("์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹คํŒจ:", error); + const errorMessage = error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; + return { + success: false, + error: errorMessage, + }; + } + } + + /** + * ๐Ÿ”ฅ Outbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ + */ + private async processOutboundMapping( + mapping: any, + sourceData: Record + ): Promise> { + try { + // ๊ฐ„๋‹จํ•œ ๋งคํ•‘ ๋กœ์ง (์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋ณ€ํ™˜ ๋กœ์ง ํ•„์š”) + const mappedData: Record = {}; + + if (mapping.fieldMappings) { + for (const fieldMapping of mapping.fieldMappings) { + const { sourceField, targetField, transformation } = fieldMapping; + + let value = sourceData[sourceField]; + + // ๋ณ€ํ™˜ ๋กœ์ง ์ ์šฉ + if (transformation) { + switch (transformation.type) { + case "format": + // ํฌ๋งท ๋ณ€ํ™˜ ๋กœ์ง + break; + case "calculate": + // ๊ณ„์‚ฐ ๋กœ์ง + break; + default: + // ๊ธฐ๋ณธ๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + break; + } + } + + mappedData[targetField] = value; + } + } + + return mappedData; + } catch (error) { + logger.error("Outbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹คํŒจ:", error); + return sourceData; // ์‹คํŒจ ์‹œ ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + } + } + + /** + * ๐Ÿ”ฅ Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ + */ + private async processInboundMapping( + mapping: any, + responseData: any + ): Promise { + try { + // Inbound ๋งคํ•‘ ๋กœ์ง (์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด๋ถ€ ์‹œ์Šคํ…œ์— ์ €์žฅ) + logger.info("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ:", mapping); + + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ๋‚ด๋ถ€ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๋กœ์ง ํ•„์š” + // ์˜ˆ: ์™ธ๋ถ€ API์—์„œ ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‚ด๋ถ€ ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์— ์—…๋ฐ์ดํŠธ + + } catch (error) { + logger.error("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹คํŒจ:", error); + // Inbound ๋งคํ•‘ ์‹คํŒจ๋Š” ์ „์ฒด ํ”Œ๋กœ์šฐ๋ฅผ ์ค‘๋‹จํ•˜์ง€ ์•Š์Œ + } + } } export default new ExternalCallConfigService(); diff --git a/backend-node/src/services/externalCallService.ts b/backend-node/src/services/externalCallService.ts index 54c0cbf9..e5cb2dd5 100644 --- a/backend-node/src/services/externalCallService.ts +++ b/backend-node/src/services/externalCallService.ts @@ -10,6 +10,11 @@ import { SupportedExternalCallSettings, TemplateOptions, } from "../types/externalCallTypes"; +import { DataMappingService } from "./dataMappingService"; +import { + DataMappingConfig, + DataMappingResult, +} from "../types/dataMappingTypes"; /** * ์™ธ๋ถ€ ํ˜ธ์ถœ ์„œ๋น„์Šค @@ -18,10 +23,149 @@ import { export class ExternalCallService { private readonly DEFAULT_TIMEOUT = 30000; // 30์ดˆ private readonly DEFAULT_RETRY_COUNT = 3; + private dataMappingService: DataMappingService; private readonly DEFAULT_RETRY_DELAY = 1000; // 1์ดˆ + constructor() { + this.dataMappingService = new DataMappingService(); + } + /** - * ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹คํ–‰ + * ๋ฐ์ดํ„ฐ ๋งคํ•‘๊ณผ ํ•จ๊ป˜ ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹คํ–‰ + */ + async executeWithDataMapping( + config: ExternalCallConfig, + dataMappingConfig?: DataMappingConfig, + triggerData?: any + ): Promise<{ + callResult: ExternalCallResult; + mappingResult?: DataMappingResult; + }> { + const startTime = Date.now(); + + console.log(`๐Ÿš€ [ExternalCallService] ๋ฐ์ดํ„ฐ ๋งคํ•‘ ํฌํ•จ ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹œ์ž‘:`, { + callType: config.callType, + hasMappingConfig: !!dataMappingConfig, + mappingDirection: dataMappingConfig?.direction, + }); + + try { + let requestData = config; + + // Outbound ๋งคํ•‘ ์ฒ˜๋ฆฌ (๋‚ด๋ถ€ โ†’ ์™ธ๋ถ€) + if ( + dataMappingConfig?.direction === "outbound" && + dataMappingConfig.outboundMapping + ) { + console.log(`๐Ÿ“ค [ExternalCallService] Outbound ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹œ์ž‘`); + + const outboundData = await this.dataMappingService.processOutboundData( + dataMappingConfig.outboundMapping, + triggerData + ); + + // API ์š”์ฒญ ๋ฐ”๋””์— ๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ ํฌํ•จ + if (config.callType === "rest-api") { + // GenericApiSettings๋กœ ํƒ€์ž… ์บ์ŠคํŒ… + const apiConfig = config as GenericApiSettings; + const bodyTemplate = apiConfig.body || "{}"; + + // ํ…œํ”Œ๋ฆฟ์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… + const processedBody = this.processTemplate(bodyTemplate, { + mappedData: outboundData, + triggerData, + ...outboundData, + }); + + requestData = { + ...config, + body: processedBody, + } as GenericApiSettings; + } + } + + // ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹คํ–‰ + const callRequest: ExternalCallRequest = { + diagramId: 0, // ์ž„์‹œ๊ฐ’ + relationshipId: "data-mapping", // ์ž„์‹œ๊ฐ’ + settings: requestData, + templateData: triggerData, + }; + const callResult = await this.executeExternalCall(callRequest); + + let mappingResult: DataMappingResult | undefined; + + // Inbound ๋งคํ•‘ ์ฒ˜๋ฆฌ (์™ธ๋ถ€ โ†’ ๋‚ด๋ถ€) + if ( + callResult.success && + dataMappingConfig?.direction === "inbound" && + dataMappingConfig.inboundMapping + ) { + console.log(`๐Ÿ“ฅ [ExternalCallService] Inbound ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹œ์ž‘`); + + try { + // ์‘๋‹ต ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ + let responseData = callResult.response; + if (typeof responseData === "string") { + try { + responseData = JSON.parse(responseData); + } catch { + console.warn( + `โš ๏ธ [ExternalCallService] ์‘๋‹ต ๋ฐ์ดํ„ฐ JSON ํŒŒ์‹ฑ ์‹คํŒจ, ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ` + ); + } + } + + mappingResult = await this.dataMappingService.processInboundData( + responseData, + dataMappingConfig.inboundMapping + ); + + console.log(`โœ… [ExternalCallService] Inbound ๋งคํ•‘ ์™„๋ฃŒ:`, { + recordsProcessed: mappingResult.recordsProcessed, + recordsInserted: mappingResult.recordsInserted, + }); + } catch (error) { + console.error(`โŒ [ExternalCallService] Inbound ๋งคํ•‘ ์‹คํŒจ:`, error); + mappingResult = { + success: false, + direction: "inbound", + errors: [error instanceof Error ? error.message : String(error)], + executionTime: Date.now() - startTime, + timestamp: new Date().toISOString(), + }; + } + } + + // ์–‘๋ฐฉํ–ฅ ๋งคํ•‘ ์ฒ˜๋ฆฌ + if (dataMappingConfig?.direction === "bidirectional") { + // ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์–‘๋ฐฉํ–ฅ ๋งคํ•‘ ๋กœ์ง ๊ตฌํ˜„ + console.log(`๐Ÿ”„ [ExternalCallService] ์–‘๋ฐฉํ–ฅ ๋งคํ•‘์€ ํ–ฅํ›„ ๊ตฌํ˜„ ์˜ˆ์ •`); + } + + const result = { + callResult, + mappingResult, + }; + + console.log(`โœ… [ExternalCallService] ๋ฐ์ดํ„ฐ ๋งคํ•‘ ํฌํ•จ ์™ธ๋ถ€ ํ˜ธ์ถœ ์™„๋ฃŒ:`, { + callSuccess: callResult.success, + mappingSuccess: mappingResult?.success, + totalExecutionTime: Date.now() - startTime, + }); + + return result; + } catch (error) { + console.error( + `โŒ [ExternalCallService] ๋ฐ์ดํ„ฐ ๋งคํ•‘ ํฌํ•จ ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹คํŒจ:`, + error + ); + throw error; + } + } + + /** + * ๊ธฐ์กด ์™ธ๋ถ€ ํ˜ธ์ถœ ์‹คํ–‰ (๋งคํ•‘ ์—†์Œ) */ async executeExternalCall( request: ExternalCallRequest diff --git a/backend-node/src/types/dataMappingTypes.ts b/backend-node/src/types/dataMappingTypes.ts new file mode 100644 index 00000000..8296fe3c --- /dev/null +++ b/backend-node/src/types/dataMappingTypes.ts @@ -0,0 +1,82 @@ +/** + * ๋ฐฑ์—”๋“œ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ๊ด€๋ จ ํƒ€์ž… ์ •์˜ + */ + +export type DataDirection = "none" | "inbound" | "outbound" | "bidirectional"; +export type InsertMode = "insert" | "upsert" | "update"; +export type TransformType = "none" | "constant" | "format" | "function"; +export type DataType = "string" | "number" | "boolean" | "date" | "json"; + +export interface FieldTransform { + type: TransformType; + value?: any; + format?: string; + functionName?: string; +} + +export interface FieldMapping { + id: string; + sourceField: string; + targetField: string; + dataType: DataType; + transform?: FieldTransform; + required?: boolean; + defaultValue?: any; +} + +export interface InboundMapping { + targetTable: string; + targetSchema?: string; + fieldMappings: FieldMapping[]; + insertMode: InsertMode; + keyFields?: string[]; + batchSize?: number; +} + +export interface OutboundMapping { + sourceTable: string; + sourceSchema?: string; + sourceFilter?: string; + fieldMappings: FieldMapping[]; + triggerCondition?: string; +} + +export interface DataMappingConfig { + direction: DataDirection; + inboundMapping?: InboundMapping; + outboundMapping?: OutboundMapping; +} + +export interface TableInfo { + name: string; + schema?: string; + displayName?: string; + fields: FieldInfo[]; +} + +export interface FieldInfo { + name: string; + dataType: DataType; + nullable: boolean; + isPrimaryKey?: boolean; + displayName?: string; + description?: string; +} + +export interface DataMappingResult { + success: boolean; + direction: DataDirection; + recordsProcessed?: number; + recordsInserted?: number; + recordsUpdated?: number; + recordsSkipped?: number; + errors?: string[]; + executionTime: number; + timestamp: string; +} + +export interface MappingValidationResult { + isValid: boolean; + errors: string[]; + warnings: string[]; +} diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index f406865c..8abc7da4 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -105,7 +105,7 @@ export default function DataFlowPage() { {/* ํŽ˜์ด์ง€ ์ œ๋ชฉ */}
-

๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ด€๋ฆฌ

+

๊ด€๊ณ„ ๊ด€๋ฆฌ

ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ์„ค๊ณ„ํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค

diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index d1eb0003..5b9989a4 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -89,7 +89,7 @@ export const DataFlowDesigner: React.FC = ({ return; }, []); - // ํŽธ์ง‘ ๋ชจ๋“œ์ผ ๋•Œ ๊ด€๊ณ„๋„ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + // ํŽธ์ง‘ ๋ชจ๋“œ์ผ ๋•Œ ๊ด€๊ณ„ ๋ฐ์ดํ„ฐ ๋กœ๋“œ useEffect(() => { const loadDiagramData = async () => { if (diagramId && diagramId > 0) { @@ -99,7 +99,7 @@ export const DataFlowDesigner: React.FC = ({ const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(diagramId, companyCode); if (jsonDiagram) { - // ๊ด€๊ณ„๋„ ์ด๋ฆ„ ์„ค์ • + // ๊ด€๊ณ„ ์ด๋ฆ„ ์„ค์ • if (jsonDiagram.diagram_name) { setCurrentDiagramName(jsonDiagram.diagram_name); } diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index f040c87d..349b72ec 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -96,14 +96,14 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { setTotal(response.pagination.total || 0); setTotalPages(Math.max(1, Math.ceil((response.pagination.total || 0) / 20))); } catch (error) { - console.error("๊ด€๊ณ„๋„ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", error); - toast.error("๊ด€๊ณ„๋„ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + console.error("๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ", error); + toast.error("๊ด€๊ณ„ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { setLoading(false); } }, [currentPage, searchTerm, companyCode]); - // ๊ด€๊ณ„๋„ ๋ชฉ๋ก ๋กœ๋“œ + // ๊ด€๊ณ„ ๋ชฉ๋ก ๋กœ๋“œ useEffect(() => { loadDiagrams(); }, [loadDiagrams]); @@ -130,13 +130,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { undefined, user?.userId || "SYSTEM", ); - toast.success(`๊ด€๊ณ„๋„๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${copiedDiagram.diagram_name}`); + toast.success(`๊ด€๊ณ„๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${copiedDiagram.diagram_name}`); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ await loadDiagrams(); } catch (error) { - console.error("๊ด€๊ณ„๋„ ๋ณต์‚ฌ ์‹คํŒจ:", error); - toast.error("๊ด€๊ณ„๋„ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + console.error("๊ด€๊ณ„ ๋ณต์‚ฌ ์‹คํŒจ:", error); + toast.error("๊ด€๊ณ„ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { setLoading(false); setShowCopyModal(false); @@ -151,13 +151,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { try { setLoading(true); await DataFlowAPI.deleteJsonDataFlowDiagram(selectedDiagramForAction.diagramId, companyCode); - toast.success(`๊ด€๊ณ„๋„๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${selectedDiagramForAction.diagramName}`); + toast.success(`๊ด€๊ณ„๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${selectedDiagramForAction.diagramName}`); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ await loadDiagrams(); } catch (error) { - console.error("๊ด€๊ณ„๋„ ์‚ญ์ œ ์‹คํŒจ:", error); - toast.error("๊ด€๊ณ„๋„ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + console.error("๊ด€๊ณ„ ์‚ญ์ œ ์‹คํŒจ:", error); + toast.error("๊ด€๊ณ„ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { setLoading(false); setShowDeleteModal(false); @@ -181,7 +181,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
setSearchTerm(e.target.value)} className="w-80 pl-10" @@ -189,17 +189,17 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
- {/* ๊ด€๊ณ„๋„ ๋ชฉ๋ก ํ…Œ์ด๋ธ” */} + {/* ๊ด€๊ณ„ ๋ชฉ๋ก ํ…Œ์ด๋ธ” */} - ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ด€๊ณ„๋„ ({total}) + ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ด€๊ณ„ ({total}) @@ -207,7 +207,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - ๊ด€๊ณ„๋„๋ช… + ๊ด€๊ณ„๋ช… ํšŒ์‚ฌ ์ฝ”๋“œ ํ…Œ์ด๋ธ” ์ˆ˜ ๊ด€๊ณ„ ์ˆ˜ @@ -284,8 +284,8 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { {diagrams.length === 0 && (
-
๊ด€๊ณ„๋„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
-
์ƒˆ ๊ด€๊ณ„๋„๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•ด๋ณด์„ธ์š”.
+
๊ด€๊ณ„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
+
์ƒˆ ๊ด€๊ณ„๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•ด๋ณด์„ธ์š”.
)} @@ -320,11 +320,11 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - ๊ด€๊ณ„๋„ ๋ณต์‚ฌ + ๊ด€๊ณ„ ๋ณต์‚ฌ - “{selectedDiagramForAction?.diagramName}” ๊ด€๊ณ„๋„๋ฅผ ๋ณต์‚ฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? + “{selectedDiagramForAction?.diagramName}” ๊ด€๊ณ„๋ฅผ ๋ณต์‚ฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?
- ์ƒˆ๋กœ์šด ๊ด€๊ณ„๋„๋Š” ์›๋ณธ ์ด๋ฆ„ ๋’ค์— (1), (2), (3)... ํ˜•ํƒœ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. + ์ƒˆ๋กœ์šด ๊ด€๊ณ„๋Š” ์›๋ณธ ์ด๋ฆ„ ๋’ค์— (1), (2), (3)... ํ˜•ํƒœ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
@@ -342,9 +342,9 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - ๊ด€๊ณ„๋„ ์‚ญ์ œ + ๊ด€๊ณ„ ์‚ญ์ œ - “{selectedDiagramForAction?.diagramName}” ๊ด€๊ณ„๋„๋ฅผ ์™„์ „ํžˆ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? + “{selectedDiagramForAction?.diagramName}” ๊ด€๊ณ„๋ฅผ ์™„์ „ํžˆ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?
์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์œผ๋ฉฐ, ๋ชจ๋“  ๊ด€๊ณ„ ์ •๋ณด๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. diff --git a/frontend/components/dataflow/DataFlowSidebar.tsx b/frontend/components/dataflow/DataFlowSidebar.tsx index 92dba462..de757462 100644 --- a/frontend/components/dataflow/DataFlowSidebar.tsx +++ b/frontend/components/dataflow/DataFlowSidebar.tsx @@ -65,7 +65,7 @@ export const DataFlowSidebar: React.FC = ({ hasUnsavedChanges ? "animate-pulse" : "" }`} > - ๐Ÿ’พ ๊ด€๊ณ„๋„ ์ €์žฅ {tempRelationships.length > 0 && `(${tempRelationships.length})`} + ๐Ÿ’พ ๊ด€๊ณ„ ์ €์žฅ {tempRelationships.length > 0 && `(${tempRelationships.length})`} diff --git a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx index 00ab5487..7a72d0d0 100644 --- a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx +++ b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx @@ -622,7 +622,57 @@ const DataConnectionDesigner: React.FC = ({ company_code: "*", // ๊ธฐ๋ณธ๊ฐ’ }; - const configResult = await ExternalCallConfigAPI.createConfig(configData); + let configResult; + + if (diagramId) { + // ์ˆ˜์ • ๋ชจ๋“œ: ๊ธฐ์กด ์„ค์ •์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์—…๋ฐ์ดํŠธ ๋˜๋Š” ์ƒ์„ฑ + console.log("๐Ÿ”„ ์ˆ˜์ • ๋ชจ๋“œ - ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ฒ˜๋ฆฌ"); + + try { + // ๋จผ์ € ๊ธฐ์กด ์„ค์ • ์กฐํšŒ ์‹œ๋„ + const existingConfigs = await ExternalCallConfigAPI.getConfigs({ + company_code: "*", + is_active: "Y", + }); + + const existingConfig = existingConfigs.data?.find( + (config: any) => config.config_name === (state.relationshipName || "์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ •") + ); + + if (existingConfig) { + // ๊ธฐ์กด ์„ค์ • ์—…๋ฐ์ดํŠธ + console.log("๐Ÿ“ ๊ธฐ์กด ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์—…๋ฐ์ดํŠธ:", existingConfig.id); + configResult = await ExternalCallConfigAPI.updateConfig(existingConfig.id, configData); + } else { + // ๊ธฐ์กด ์„ค์ •์ด ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + console.log("๐Ÿ†• ์ƒˆ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ƒ์„ฑ (์ˆ˜์ • ๋ชจ๋“œ)"); + configResult = await ExternalCallConfigAPI.createConfig(configData); + } + } catch (updateError) { + // ์ค‘๋ณต ์ƒ์„ฑ ์˜ค๋ฅ˜์ธ ๊ฒฝ์šฐ ๋ฌด์‹œํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰ + if (updateError.message && updateError.message.includes("์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค")) { + console.log("โš ๏ธ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ •์ด ์ด๋ฏธ ์กด์žฌํ•จ - ๊ธฐ์กด ์„ค์ • ์‚ฌ์šฉ"); + configResult = { success: true, message: "๊ธฐ์กด ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์‚ฌ์šฉ" }; + } else { + console.warn("โš ๏ธ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ฒ˜๋ฆฌ ์‹คํŒจ:", updateError); + throw updateError; + } + } + } else { + // ์‹ ๊ทœ ์ƒ์„ฑ ๋ชจ๋“œ + console.log("๐Ÿ†• ์‹ ๊ทœ ์ƒ์„ฑ ๋ชจ๋“œ - ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ƒ์„ฑ"); + try { + configResult = await ExternalCallConfigAPI.createConfig(configData); + } catch (createError) { + // ์ค‘๋ณต ์ƒ์„ฑ ์˜ค๋ฅ˜์ธ ๊ฒฝ์šฐ ๋ฌด์‹œํ•˜๊ณ  ๊ณ„์† ์ง„ํ–‰ + if (createError.message && createError.message.includes("์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค")) { + console.log("โš ๏ธ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ •์ด ์ด๋ฏธ ์กด์žฌํ•จ - ๊ธฐ์กด ์„ค์ • ์‚ฌ์šฉ"); + configResult = { success: true, message: "๊ธฐ์กด ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์‚ฌ์šฉ" }; + } else { + throw createError; + } + } + } if (!configResult.success) { throw new Error(configResult.error || "์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ €์žฅ ์‹คํŒจ"); diff --git a/frontend/components/dataflow/external-call/DataMappingSettings.tsx b/frontend/components/dataflow/external-call/DataMappingSettings.tsx new file mode 100644 index 00000000..a4e1ea56 --- /dev/null +++ b/frontend/components/dataflow/external-call/DataMappingSettings.tsx @@ -0,0 +1,394 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, Database, ArrowRight, Settings } from "lucide-react"; + +import { + DataMappingConfig, + DataDirection, + TableInfo, + FieldMapping, + InboundMapping, + OutboundMapping, + DATA_DIRECTION_OPTIONS, + INSERT_MODE_OPTIONS, +} from "@/types/external-call/DataMappingTypes"; + +import { FieldMappingEditor } from "./FieldMappingEditor"; + +interface DataMappingSettingsProps { + config: DataMappingConfig; + onConfigChange: (config: DataMappingConfig) => void; + httpMethod: string; + availableTables?: TableInfo[]; + readonly?: boolean; + tablesLoading?: boolean; +} + +export const DataMappingSettings: React.FC = ({ + config, + onConfigChange, + httpMethod, + availableTables = [], + readonly = false, + tablesLoading = false, +}) => { + const [localConfig, setLocalConfig] = useState(config); + + // ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ๋™๊ธฐํ™” + useEffect(() => { + setLocalConfig(config); + }, [config]); + + // HTTP ๋ฉ”์„œ๋“œ์— ๋”ฐ๋ฅธ ๊ถŒ์žฅ ๋ฐฉํ–ฅ ๊ฒฐ์ • + const getRecommendedDirection = useCallback((method: string): DataDirection => { + const upperMethod = method.toUpperCase(); + if (upperMethod === "GET") return "inbound"; + if (["POST", "PUT", "PATCH"].includes(upperMethod)) return "outbound"; + return "none"; + }, []); + + // ๋ฐฉํ–ฅ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleDirectionChange = useCallback( + (direction: DataDirection) => { + const newConfig = { + ...localConfig, + direction, + // ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ ๋ถˆํ•„์š”ํ•œ ๋งคํ•‘ ์ œ๊ฑฐ + inboundMapping: + direction === "inbound" || direction === "bidirectional" + ? localConfig.inboundMapping || { + targetTable: "", + fieldMappings: [], + insertMode: "insert" as const, + } + : undefined, + outboundMapping: + direction === "outbound" || direction === "bidirectional" + ? localConfig.outboundMapping || { + sourceTable: "", + fieldMappings: [], + } + : undefined, + }; + setLocalConfig(newConfig); + onConfigChange(newConfig); + }, + [localConfig, onConfigChange], + ); + + // Inbound ๋งคํ•‘ ์—…๋ฐ์ดํŠธ + const handleInboundMappingChange = useCallback( + (mapping: Partial) => { + const newConfig = { + ...localConfig, + inboundMapping: { + ...localConfig.inboundMapping!, + ...mapping, + }, + }; + setLocalConfig(newConfig); + onConfigChange(newConfig); + }, + [localConfig, onConfigChange], + ); + + // Outbound ๋งคํ•‘ ์—…๋ฐ์ดํŠธ + const handleOutboundMappingChange = useCallback( + (mapping: Partial) => { + const newConfig = { + ...localConfig, + outboundMapping: { + ...localConfig.outboundMapping!, + ...mapping, + }, + }; + setLocalConfig(newConfig); + onConfigChange(newConfig); + }, + [localConfig, onConfigChange], + ); + + // ํ•„๋“œ ๋งคํ•‘ ์—…๋ฐ์ดํŠธ (Inbound) + const handleInboundFieldMappingsChange = useCallback( + (fieldMappings: FieldMapping[]) => { + handleInboundMappingChange({ fieldMappings }); + }, + [handleInboundMappingChange], + ); + + // ํ•„๋“œ ๋งคํ•‘ ์—…๋ฐ์ดํŠธ (Outbound) + const handleOutboundFieldMappingsChange = useCallback( + (fieldMappings: FieldMapping[]) => { + handleOutboundMappingChange({ fieldMappings }); + }, + [handleOutboundMappingChange], + ); + + // ๊ฒ€์ฆ ํ•จ์ˆ˜ + const isConfigValid = useCallback(() => { + if (localConfig.direction === "none") return true; + + if ( + (localConfig.direction === "inbound" || localConfig.direction === "bidirectional") && + localConfig.inboundMapping + ) { + if (!localConfig.inboundMapping.targetTable) return false; + if (localConfig.inboundMapping.fieldMappings.length === 0) return false; + } + + if ( + (localConfig.direction === "outbound" || localConfig.direction === "bidirectional") && + localConfig.outboundMapping + ) { + if (!localConfig.outboundMapping.sourceTable) return false; + if (localConfig.outboundMapping.fieldMappings.length === 0) return false; + } + + return true; + }, [localConfig]); + + const recommendedDirection = getRecommendedDirection(httpMethod); + + return ( + + + + + ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • + {!isConfigValid() && ์„ค์ • ํ•„์š”} + {isConfigValid() && localConfig.direction !== "none" && ์„ค์ • ์™„๋ฃŒ} + +

์™ธ๋ถ€ API์™€ ๋‚ด๋ถ€ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ๋งคํ•‘์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

+
+ + {/* ๋งคํ•‘ ๋ฐฉํ–ฅ ์„ ํƒ */} +
+ + + {localConfig.direction !== recommendedDirection && recommendedDirection !== "none" && ( +

+ ๐Ÿ’ก {httpMethod} ์š”์ฒญ์—๋Š” "{DATA_DIRECTION_OPTIONS.find((o) => o.value === recommendedDirection)?.label}" + ๋ฐฉํ–ฅ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค. +

+ )} +
+ + {/* ๋งคํ•‘ ์„ค์ • ํƒญ */} + {localConfig.direction !== "none" && ( + + + {(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") && ( + + + ์™ธ๋ถ€ โ†’ ๋‚ด๋ถ€ + + )} + {(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") && ( + + + ๋‚ด๋ถ€ โ†’ ์™ธ๋ถ€ + + )} + + + {/* Inbound ๋งคํ•‘ ์„ค์ • */} + {(localConfig.direction === "inbound" || localConfig.direction === "bidirectional") && ( + +
+
+ + +
+ +
+ + +
+
+ + {/* ํ‚ค ํ•„๋“œ ์„ค์ • (upsert/update ๋ชจ๋“œ์ผ ๋•Œ) */} + {localConfig.inboundMapping?.insertMode !== "insert" && ( +
+ + + handleInboundMappingChange({ + keyFields: e.target.value + .split(",") + .map((s) => s.trim()) + .filter(Boolean), + }) + } + placeholder="id, code" + disabled={readonly} + /> +

+ ์—…๋ฐ์ดํŠธ/์—…์„œํŠธ ์‹œ ์‚ฌ์šฉํ•  ํ‚ค ํ•„๋“œ๋ฅผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ž…๋ ฅํ•˜์„ธ์š”. +

+
+ )} + + {/* ํ•„๋“œ ๋งคํ•‘ ์—๋””ํ„ฐ */} + {localConfig.inboundMapping?.targetTable && ( + t.name === localConfig.inboundMapping?.targetTable)} + readonly={readonly} + /> + )} +
+ )} + + {/* Outbound ๋งคํ•‘ ์„ค์ • */} + {(localConfig.direction === "outbound" || localConfig.direction === "bidirectional") && ( + +
+
+ + +
+
+ + {/* ์†Œ์Šค ํ•„ํ„ฐ ์กฐ๊ฑด */} +
+ +