From 74ebb565e6a38ed1812e19370eec73d45d1197f5 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 21 Oct 2025 14:21:29 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/flowController.ts | 7 ++- backend-node/src/routes/flowRoutes.ts | 5 +- backend-node/src/services/externalDbHelper.ts | 22 ++----- .../src/services/flowDataMoveService.ts | 61 +++++++++++++++++-- backend-node/src/types/flow.ts | 3 + .../components/flow/FlowConditionBuilder.tsx | 22 ++++--- frontend/components/flow/FlowStepPanel.tsx | 55 ++++++++++------- .../components/screen/widgets/FlowWidget.tsx | 16 +++++ frontend/lib/api/flow.ts | 10 +++ frontend/types/flow.ts | 9 +++ 10 files changed, 154 insertions(+), 56 deletions(-) diff --git a/backend-node/src/controllers/flowController.ts b/backend-node/src/controllers/flowController.ts index 398dd414..f596af97 100644 --- a/backend-node/src/controllers/flowController.ts +++ b/backend-node/src/controllers/flowController.ts @@ -31,13 +31,16 @@ export class FlowController { */ createFlowDefinition = async (req: Request, res: Response): Promise => { try { - const { name, description, tableName } = req.body; + const { name, description, tableName, dbSourceType, dbConnectionId } = + req.body; const userId = (req as any).user?.userId || "system"; console.log("๐Ÿ” createFlowDefinition called with:", { name, description, tableName, + dbSourceType, + dbConnectionId, }); if (!name) { @@ -62,7 +65,7 @@ export class FlowController { } const flowDef = await this.flowDefinitionService.create( - { name, description, tableName }, + { name, description, tableName, dbSourceType, dbConnectionId }, userId ); diff --git a/backend-node/src/routes/flowRoutes.ts b/backend-node/src/routes/flowRoutes.ts index 93c59ad1..06c6795b 100644 --- a/backend-node/src/routes/flowRoutes.ts +++ b/backend-node/src/routes/flowRoutes.ts @@ -4,6 +4,7 @@ import { Router } from "express"; import { FlowController } from "../controllers/flowController"; +import { authenticateToken } from "../middleware/authMiddleware"; const router = Router(); const flowController = new FlowController(); @@ -32,8 +33,8 @@ router.get("/:flowId/step/:stepId/list", flowController.getStepDataList); router.get("/:flowId/steps/counts", flowController.getAllStepCounts); // ==================== ๋ฐ์ดํ„ฐ ์ด๋™ ==================== -router.post("/move", flowController.moveData); -router.post("/move-batch", flowController.moveBatchData); +router.post("/move", authenticateToken, flowController.moveData); +router.post("/move-batch", authenticateToken, flowController.moveBatchData); // ==================== ์˜ค๋”ง ๋กœ๊ทธ ==================== router.get("/audit/:flowId/:recordId", flowController.getAuditLogs); diff --git a/backend-node/src/services/externalDbHelper.ts b/backend-node/src/services/externalDbHelper.ts index 2a774ef2..9a8b4f7d 100644 --- a/backend-node/src/services/externalDbHelper.ts +++ b/backend-node/src/services/externalDbHelper.ts @@ -7,7 +7,7 @@ import { Pool as PgPool } from "pg"; import * as mysql from "mysql2/promise"; import db from "../database/db"; -import { CredentialEncryption } from "../utils/credentialEncryption"; +import { PasswordEncryption } from "../utils/passwordEncryption"; import { getConnectionTestQuery, getPlaceholder, @@ -31,24 +31,13 @@ interface ExternalDbConnection { // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ’€ ์บ์‹œ (ํƒ€์ž…๋ณ„๋กœ ๋‹ค๋ฅธ ํ’€ ๊ฐ์ฒด) const connectionPools = new Map(); -// ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” ์œ ํ‹ธ -const credentialEncryption = new CredentialEncryption( - process.env.ENCRYPTION_SECRET_KEY || "default-secret-key-change-in-production" -); - /** * ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ •๋ณด ์กฐํšŒ */ async function getExternalConnection( connectionId: number ): Promise { - const query = ` - SELECT - id, connection_name, db_type, host, port, - database_name, username, encrypted_password, is_active - FROM external_db_connections - WHERE id = $1 AND is_active = true - `; + const query = `SELECT * FROM external_db_connections WHERE id = $1 AND is_active = 'Y'`; const result = await db.query(query, [connectionId]); @@ -58,13 +47,14 @@ async function getExternalConnection( const row = result[0]; - // ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” + // ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” (์•”ํ˜ธํ™”๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” password ์ปฌ๋Ÿผ์— ์ €์žฅ๋จ) let decryptedPassword = ""; try { - decryptedPassword = credentialEncryption.decrypt(row.encrypted_password); + decryptedPassword = PasswordEncryption.decrypt(row.password); } catch (error) { console.error(`๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” ์‹คํŒจ (ID: ${connectionId}):`, error); - throw new Error("์™ธ๋ถ€ DB ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"); + // ๋ณตํ˜ธํ™” ์‹คํŒจ ์‹œ ์›๋ณธ ๋น„๋ฐ€๋ฒˆํ˜ธ ์‚ฌ์šฉ (fallback) + decryptedPassword = row.password; } return { diff --git a/backend-node/src/services/flowDataMoveService.ts b/backend-node/src/services/flowDataMoveService.ts index 03fd18c6..39ab6013 100644 --- a/backend-node/src/services/flowDataMoveService.ts +++ b/backend-node/src/services/flowDataMoveService.ts @@ -161,6 +161,28 @@ export class FlowDataMoveService { } // 5. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก + let dbConnectionName = null; + if ( + flowDefinition.dbSourceType === "external" && + flowDefinition.dbConnectionId + ) { + // ์™ธ๋ถ€ DB์ธ ๊ฒฝ์šฐ ์—ฐ๊ฒฐ ์ด๋ฆ„ ์กฐํšŒ + try { + const connResult = await client.query( + `SELECT connection_name FROM external_db_connections WHERE id = $1`, + [flowDefinition.dbConnectionId] + ); + if (connResult.rows && connResult.rows.length > 0) { + dbConnectionName = connResult.rows[0].connection_name; + } + } catch (error) { + console.warn("์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ด๋ฆ„ ์กฐํšŒ ์‹คํŒจ:", error); + } + } else { + // ๋‚ด๋ถ€ DB์ธ ๊ฒฝ์šฐ + dbConnectionName = "๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค"; + } + await this.logDataMove(client, { flowId, fromStepId, @@ -173,6 +195,11 @@ export class FlowDataMoveService { statusFrom: fromStep.statusValue, statusTo: toStep.statusValue, userId, + dbConnectionId: + flowDefinition.dbSourceType === "external" + ? flowDefinition.dbConnectionId + : null, + dbConnectionName, }); return { @@ -361,8 +388,9 @@ export class FlowDataMoveService { move_type, source_table, target_table, source_data_id, target_data_id, status_from, status_to, - changed_by, note - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + changed_by, note, + db_connection_id, db_connection_name + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) `; await client.query(query, [ @@ -378,6 +406,8 @@ export class FlowDataMoveService { params.statusTo, params.userId, params.note || null, + params.dbConnectionId || null, + params.dbConnectionName || null, ]); } @@ -452,6 +482,8 @@ export class FlowDataMoveService { targetDataId: row.target_data_id, statusFrom: row.status_from, statusTo: row.status_to, + dbConnectionId: row.db_connection_id, + dbConnectionName: row.db_connection_name, })); } @@ -496,6 +528,8 @@ export class FlowDataMoveService { targetDataId: row.target_data_id, statusFrom: row.status_from, statusTo: row.status_to, + dbConnectionId: row.db_connection_id, + dbConnectionName: row.db_connection_name, })); } @@ -718,7 +752,21 @@ export class FlowDataMoveService { // 3. ์™ธ๋ถ€ ์—ฐ๋™ ์ฒ˜๋ฆฌ๋Š” ์ƒ๋žต (์™ธ๋ถ€ DB ์ž์ฒด๊ฐ€ ์™ธ๋ถ€์ด๋ฏ€๋กœ) - // 4. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก (๋‚ด๋ถ€ DB์—) + // 4. ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ด๋ฆ„ ์กฐํšŒ + let dbConnectionName = null; + try { + const connResult = await db.query( + `SELECT connection_name FROM external_db_connections WHERE id = $1`, + [dbConnectionId] + ); + if (connResult.length > 0) { + dbConnectionName = connResult[0].connection_name; + } + } catch (error) { + console.warn("์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ด๋ฆ„ ์กฐํšŒ ์‹คํŒจ:", error); + } + + // 5. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก (๋‚ด๋ถ€ DB์—) // ์™ธ๋ถ€ DB๋Š” ๋‚ด๋ถ€ DB ํŠธ๋žœ์žญ์…˜ ์™ธ๋ถ€์ด๋ฏ€๋กœ ์ง์ ‘ ์ฟผ๋ฆฌ ์‹คํ–‰ const auditQuery = ` INSERT INTO flow_audit_log ( @@ -726,8 +774,9 @@ export class FlowDataMoveService { move_type, source_table, target_table, source_data_id, target_data_id, status_from, status_to, - changed_by, note - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + changed_by, note, + db_connection_id, db_connection_name + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) `; await db.query(auditQuery, [ @@ -743,6 +792,8 @@ export class FlowDataMoveService { toStep.statusValue || null, // statusTo userId, `์™ธ๋ถ€ DB (${dbType}) ๋ฐ์ดํ„ฐ ์ด๋™`, + dbConnectionId, + dbConnectionName, ]); return { diff --git a/backend-node/src/types/flow.ts b/backend-node/src/types/flow.ts index 0c84fbeb..4368ae1a 100644 --- a/backend-node/src/types/flow.ts +++ b/backend-node/src/types/flow.ts @@ -182,6 +182,9 @@ export interface FlowAuditLog { targetDataId?: string; statusFrom?: string; statusTo?: string; + // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ •๋ณด + dbConnectionId?: number; + dbConnectionName?: string; // ์กฐ์ธ ํ•„๋“œ fromStepName?: string; toStepName?: string; diff --git a/frontend/components/flow/FlowConditionBuilder.tsx b/frontend/components/flow/FlowConditionBuilder.tsx index c3572dc8..0440d049 100644 --- a/frontend/components/flow/FlowConditionBuilder.tsx +++ b/frontend/components/flow/FlowConditionBuilder.tsx @@ -243,14 +243,20 @@ export function FlowConditionBuilder({ - {columns.map((col) => ( - -
- {col.displayName || col.columnName} - ({col.dataType}) -
-
- ))} + {columns.map((col, idx) => { + const columnName = col.column_name || col.columnName || ""; + const dataType = col.data_type || col.dataType || ""; + const displayName = col.displayName || col.display_name || columnName; + + return ( + +
+ {displayName} + ({dataType}) +
+
+ ); + })}
)} diff --git a/frontend/components/flow/FlowStepPanel.tsx b/frontend/components/flow/FlowStepPanel.tsx index 21a660bb..8dc59a28 100644 --- a/frontend/components/flow/FlowStepPanel.tsx +++ b/frontend/components/flow/FlowStepPanel.tsx @@ -666,8 +666,12 @@ export function FlowStepPanel({ {loadingColumns ? "์ปฌ๋Ÿผ ๋กœ๋”ฉ ์ค‘..." : formData.statusColumn - ? columns.find((col) => col.columnName === formData.statusColumn)?.columnName || - formData.statusColumn + ? (() => { + const col = columns.find( + (c) => (c.column_name || c.columnName) === formData.statusColumn, + ); + return col ? col.column_name || col.columnName : formData.statusColumn; + })() : "์ƒํƒœ ์ปฌ๋Ÿผ ์„ ํƒ"} @@ -678,27 +682,32 @@ export function FlowStepPanel({ ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {columns.map((column) => ( - { - setFormData({ ...formData, statusColumn: column.columnName }); - setOpenStatusColumnCombobox(false); - }} - > - -
-
{column.columnName}
-
({column.dataType})
-
-
- ))} + {columns.map((column, idx) => { + const columnName = column.column_name || column.columnName || ""; + const dataType = column.data_type || column.dataType || ""; + + return ( + { + setFormData({ ...formData, statusColumn: columnName }); + setOpenStatusColumnCombobox(false); + }} + > + +
+
{columnName}
+
({dataType})
+
+
+ ); + })}
diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index dc4ad00e..152e233e 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -397,6 +397,7 @@ export function FlowWidget({ component, onStepClick }: FlowWidgetProps) { ๋ฐ์ดํ„ฐ ID ์ƒํƒœ ๋ณ€๊ฒฝ ๋ณ€๊ฒฝ์ž + DB ์—ฐ๊ฒฐ ํ…Œ์ด๋ธ” @@ -450,6 +451,21 @@ export function FlowWidget({ component, onStepClick }: FlowWidgetProps) { )} {log.changedBy} + + {log.dbConnectionName ? ( + + {log.dbConnectionName} + + ) : ( + - + )} + {log.sourceTable || "-"} {log.targetTable && log.targetTable !== log.sourceTable && ( diff --git a/frontend/lib/api/flow.ts b/frontend/lib/api/flow.ts index 08fb553b..7d61293a 100644 --- a/frontend/lib/api/flow.ts +++ b/frontend/lib/api/flow.ts @@ -21,6 +21,12 @@ import { const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api"; +// ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ +function getAuthToken(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem("authToken") || sessionStorage.getItem("authToken"); +} + // ============================================ // ํ”Œ๋กœ์šฐ ์ •์˜ API // ============================================ @@ -364,10 +370,12 @@ export async function getAllStepCounts(flowId: number): Promise> { try { + const token = getAuthToken(); const response = await fetch(`${API_BASE}/flow/move`, { method: "POST", headers: { "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), }, credentials: "include", body: JSON.stringify(data), @@ -404,10 +412,12 @@ export async function moveBatchData( data: MoveBatchDataRequest, ): Promise> { try { + const token = getAuthToken(); const response = await fetch(`${API_BASE}/flow/move-batch`, { method: "POST", headers: { "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), }, credentials: "include", body: JSON.stringify(data), diff --git a/frontend/types/flow.ts b/frontend/types/flow.ts index d6f05a5e..5fd17e66 100644 --- a/frontend/types/flow.ts +++ b/frontend/types/flow.ts @@ -148,6 +148,15 @@ export interface FlowAuditLog { note?: string; fromStepName?: string; toStepName?: string; + moveType?: "status" | "table" | "both"; + sourceTable?: string; + targetTable?: string; + sourceDataId?: string; + targetDataId?: string; + statusFrom?: string; + statusTo?: string; + dbConnectionId?: number; + dbConnectionName?: string; } // ============================================