diff --git a/backend-node/src/controllers/flowController.ts b/backend-node/src/controllers/flowController.ts index e555e6f7..398dd414 100644 --- a/backend-node/src/controllers/flowController.ts +++ b/backend-node/src/controllers/flowController.ts @@ -34,23 +34,31 @@ export class FlowController { const { name, description, tableName } = req.body; const userId = (req as any).user?.userId || "system"; - if (!name || !tableName) { + console.log("๐Ÿ” createFlowDefinition called with:", { + name, + description, + tableName, + }); + + if (!name) { res.status(400).json({ success: false, - message: "Name and tableName are required", + message: "Name is required", }); return; } - // ํ…Œ์ด๋ธ” ์กด์žฌ ํ™•์ธ - const tableExists = - await this.flowDefinitionService.checkTableExists(tableName); - if (!tableExists) { - res.status(400).json({ - success: false, - message: `Table '${tableName}' does not exist`, - }); - return; + // ํ…Œ์ด๋ธ” ์ด๋ฆ„์ด ์ œ๊ณต๋œ ๊ฒฝ์šฐ์—๋งŒ ์กด์žฌ ํ™•์ธ + if (tableName) { + const tableExists = + await this.flowDefinitionService.checkTableExists(tableName); + if (!tableExists) { + res.status(400).json({ + success: false, + message: `Table '${tableName}' does not exist`, + }); + return; + } } const flowDef = await this.flowDefinitionService.create( @@ -294,6 +302,13 @@ export class FlowController { color, positionX, positionY, + moveType, + statusColumn, + statusValue, + targetTable, + fieldMappings, + integrationType, + integrationConfig, } = req.body; const step = await this.flowStepService.update(id, { @@ -304,6 +319,13 @@ export class FlowController { color, positionX, positionY, + moveType, + statusColumn, + statusValue, + targetTable, + fieldMappings, + integrationType, + integrationConfig, }); if (!step) { diff --git a/backend-node/src/services/dbQueryBuilder.ts b/backend-node/src/services/dbQueryBuilder.ts new file mode 100644 index 00000000..e7b8d9bc --- /dev/null +++ b/backend-node/src/services/dbQueryBuilder.ts @@ -0,0 +1,230 @@ +/** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ณ„ ์ฟผ๋ฆฌ ๋นŒ๋” + * PostgreSQL, MySQL/MariaDB, MSSQL, Oracle ์ง€์› + */ + +export type DbType = "postgresql" | "mysql" | "mariadb" | "mssql" | "oracle"; + +/** + * DB๋ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ƒ์„ฑ + */ +export function getPlaceholder(dbType: string, index: number): string { + const normalizedType = dbType.toLowerCase(); + + switch (normalizedType) { + case "postgresql": + return `$${index}`; + + case "mysql": + case "mariadb": + return "?"; + + case "mssql": + return `@p${index}`; + + case "oracle": + return `:${index}`; + + default: + // ๊ธฐ๋ณธ๊ฐ’์€ PostgreSQL + return `$${index}`; + } +} + +/** + * UPDATE ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ +export function buildUpdateQuery( + dbType: string, + tableName: string, + updates: { column: string; value: any }[], + whereColumn: string = "id" +): { query: string; values: any[] } { + const normalizedType = dbType.toLowerCase(); + const values: any[] = []; + + // SET ์ ˆ ์ƒ์„ฑ + const setClause = updates + .map((update, index) => { + values.push(update.value); + const placeholder = getPlaceholder(normalizedType, values.length); + return `${update.column} = ${placeholder}`; + }) + .join(", "); + + // WHERE ์ ˆ ์ƒ์„ฑ + values.push(undefined); // whereValue๋Š” ๋‚˜์ค‘์— ์„ค์ • + const wherePlaceholder = getPlaceholder(normalizedType, values.length); + + // updated_at ์ฒ˜๋ฆฌ (DB๋ณ„ NOW() ํ•จ์ˆ˜) + let updatedAtExpr = "NOW()"; + if (normalizedType === "mssql") { + updatedAtExpr = "GETDATE()"; + } else if (normalizedType === "oracle") { + updatedAtExpr = "SYSDATE"; + } + + const query = ` + UPDATE ${tableName} + SET ${setClause}, updated_at = ${updatedAtExpr} + WHERE ${whereColumn} = ${wherePlaceholder} + `; + + return { query, values }; +} + +/** + * INSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ +export function buildInsertQuery( + dbType: string, + tableName: string, + data: Record +): { query: string; values: any[]; returningClause: string } { + const normalizedType = dbType.toLowerCase(); + const columns = Object.keys(data); + const values = Object.values(data); + + // ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ƒ์„ฑ + const placeholders = columns + .map((_, index) => getPlaceholder(normalizedType, index + 1)) + .join(", "); + + let query = ` + INSERT INTO ${tableName} (${columns.join(", ")}) + VALUES (${placeholders}) + `; + + // RETURNING/OUTPUT ์ ˆ ์ถ”๊ฐ€ (DB๋ณ„๋กœ ๋‹ค๋ฆ„) + let returningClause = ""; + if (normalizedType === "postgresql") { + query += " RETURNING id"; + returningClause = "RETURNING id"; + } else if (normalizedType === "mssql") { + // MSSQL์€ OUTPUT ์ ˆ์„ INSERT์™€ VALUES ์‚ฌ์ด์— + const insertIndex = query.indexOf("VALUES"); + query = + query.substring(0, insertIndex) + + "OUTPUT INSERTED.id " + + query.substring(insertIndex); + returningClause = "OUTPUT INSERTED.id"; + } else if (normalizedType === "oracle") { + query += " RETURNING id INTO :out_id"; + returningClause = "RETURNING id INTO :out_id"; + } + // MySQL/MariaDB๋Š” RETURNING ์—†์Œ, LAST_INSERT_ID() ์‚ฌ์šฉ + + return { query, values, returningClause }; +} + +/** + * SELECT ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ +export function buildSelectQuery( + dbType: string, + tableName: string, + whereColumn: string = "id" +): { query: string; placeholder: string } { + const normalizedType = dbType.toLowerCase(); + const placeholder = getPlaceholder(normalizedType, 1); + + const query = `SELECT * FROM ${tableName} WHERE ${whereColumn} = ${placeholder}`; + + return { query, placeholder }; +} + +/** + * LIMIT/OFFSET ์ฟผ๋ฆฌ ์ƒ์„ฑ (ํŽ˜์ด์ง•) + */ +export function buildPaginationClause( + dbType: string, + limit?: number, + offset?: number +): string { + const normalizedType = dbType.toLowerCase(); + + if (!limit) { + return ""; + } + + if ( + normalizedType === "postgresql" || + normalizedType === "mysql" || + normalizedType === "mariadb" + ) { + // PostgreSQL, MySQL, MariaDB: LIMIT ... OFFSET ... + let clause = ` LIMIT ${limit}`; + if (offset) { + clause += ` OFFSET ${offset}`; + } + return clause; + } else if (normalizedType === "mssql") { + // MSSQL: OFFSET ... ROWS FETCH NEXT ... ROWS ONLY + if (offset) { + return ` OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY`; + } else { + return ` OFFSET 0 ROWS FETCH NEXT ${limit} ROWS ONLY`; + } + } else if (normalizedType === "oracle") { + // Oracle: ROWNUM ๋˜๋Š” FETCH FIRST (12c+) + if (offset) { + return ` OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY`; + } else { + return ` FETCH FIRST ${limit} ROWS ONLY`; + } + } + + return ""; +} + +/** + * ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + */ +export function getBeginTransactionQuery(dbType: string): string { + const normalizedType = dbType.toLowerCase(); + + if (normalizedType === "mssql") { + return "BEGIN TRANSACTION"; + } + + return "BEGIN"; +} + +/** + * ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ + */ +export function getCommitQuery(dbType: string): string { + return "COMMIT"; +} + +/** + * ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ + */ +export function getRollbackQuery(dbType: string): string { + return "ROLLBACK"; +} + +/** + * DB ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์ฟผ๋ฆฌ + */ +export function getConnectionTestQuery(dbType: string): string { + const normalizedType = dbType.toLowerCase(); + + switch (normalizedType) { + case "postgresql": + return "SELECT 1"; + + case "mysql": + case "mariadb": + return "SELECT 1"; + + case "mssql": + return "SELECT 1"; + + case "oracle": + return "SELECT 1 FROM DUAL"; + + default: + return "SELECT 1"; + } +} diff --git a/backend-node/src/services/externalDbHelper.ts b/backend-node/src/services/externalDbHelper.ts new file mode 100644 index 00000000..2a774ef2 --- /dev/null +++ b/backend-node/src/services/externalDbHelper.ts @@ -0,0 +1,477 @@ +/** + * ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ—ฌํผ + * ํ”Œ๋กœ์šฐ ๋ฐ์ดํ„ฐ ์ด๋™ ์‹œ ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ๊ด€๋ฆฌ + * PostgreSQL, MySQL/MariaDB, MSSQL, Oracle ์ง€์› + */ + +import { Pool as PgPool } from "pg"; +import * as mysql from "mysql2/promise"; +import db from "../database/db"; +import { CredentialEncryption } from "../utils/credentialEncryption"; +import { + getConnectionTestQuery, + getPlaceholder, + getBeginTransactionQuery, + getCommitQuery, + getRollbackQuery, +} from "./dbQueryBuilder"; + +interface ExternalDbConnection { + id: number; + connectionName: string; + dbType: string; + host: string; + port: number; + database: string; + username: string; + password: string; + isActive: boolean; +} + +// ์™ธ๋ถ€ 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 result = await db.query(query, [connectionId]); + + if (result.length === 0) { + return null; + } + + const row = result[0]; + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” + let decryptedPassword = ""; + try { + decryptedPassword = credentialEncryption.decrypt(row.encrypted_password); + } catch (error) { + console.error(`๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™” ์‹คํŒจ (ID: ${connectionId}):`, error); + throw new Error("์™ธ๋ถ€ DB ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณตํ˜ธํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"); + } + + return { + id: row.id, + connectionName: row.connection_name, + dbType: row.db_type, + host: row.host, + port: row.port, + database: row.database_name, + username: row.username, + password: decryptedPassword, + isActive: row.is_active, + }; +} + +/** + * ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ ๋˜๋Š” ์žฌ์‚ฌ์šฉ + */ +export async function getExternalPool(connectionId: number): Promise { + // ์บ์‹œ๋œ ์—ฐ๊ฒฐ ํ’€ ํ™•์ธ + if (connectionPools.has(connectionId)) { + const poolInfo = connectionPools.get(connectionId)!; + const connection = await getExternalConnection(connectionId); + + // ์—ฐ๊ฒฐ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ + try { + const testQuery = getConnectionTestQuery(connection!.dbType); + await executePoolQuery(poolInfo.pool, connection!.dbType, testQuery, []); + return poolInfo; + } catch (error) { + console.warn( + `์บ์‹œ๋œ ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ’€ ๋ฌดํšจํ™” (ID: ${connectionId}), ์žฌ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.` + ); + connectionPools.delete(connectionId); + await closePool(poolInfo.pool, connection!.dbType); + } + } + + // ์ƒˆ๋กœ์šด ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ + const connection = await getExternalConnection(connectionId); + + if (!connection) { + throw new Error( + `์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (ID: ${connectionId})` + ); + } + + const dbType = connection.dbType.toLowerCase(); + let pool: any; + + try { + switch (dbType) { + case "postgresql": + pool = await createPostgreSQLPool(connection); + break; + + case "mysql": + case "mariadb": + pool = await createMySQLPool(connection); + break; + + case "mssql": + pool = await createMSSQLPool(connection); + break; + + case "oracle": + pool = await createOraclePool(connection); + break; + + default: + throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” DB ํƒ€์ž…์ž…๋‹ˆ๋‹ค: ${connection.dbType}`); + } + + // ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ + const testQuery = getConnectionTestQuery(dbType); + await executePoolQuery(pool, dbType, testQuery, []); + + console.log( + `โœ… ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ ์„ฑ๊ณต (ID: ${connectionId}, ${connection.connectionName}, ${connection.dbType})` + ); + + // ์บ์‹œ์— ์ €์žฅ (dbType ์ •๋ณด ํฌํ•จ) + const poolInfo = { pool, dbType }; + connectionPools.set(connectionId, poolInfo); + + return poolInfo; + } catch (error) { + if (pool) { + await closePool(pool, dbType); + } + throw new Error( + `์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์‹คํŒจ (${connection.connectionName}, ${connection.dbType}): ${error}` + ); + } +} + +/** + * PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ + */ +async function createPostgreSQLPool( + connection: ExternalDbConnection +): Promise { + return new PgPool({ + host: connection.host, + port: connection.port, + database: connection.database, + user: connection.username, + password: connection.password, + max: 5, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + }); +} + +/** + * MySQL/MariaDB ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ + */ +async function createMySQLPool( + connection: ExternalDbConnection +): Promise { + return mysql.createPool({ + host: connection.host, + port: connection.port, + database: connection.database, + user: connection.username, + password: connection.password, + connectionLimit: 5, + waitForConnections: true, + queueLimit: 0, + }); +} + +/** + * MSSQL ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ + */ +async function createMSSQLPool(connection: ExternalDbConnection): Promise { + // mssql ํŒจํ‚ค์ง€๋ฅผ ๋™์ ์œผ๋กœ import (์„ค์น˜๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ) + try { + const sql = require("mssql"); + const config = { + user: connection.username, + password: connection.password, + server: connection.host, + port: connection.port, + database: connection.database, + options: { + encrypt: true, + trustServerCertificate: true, + enableArithAbort: true, + }, + pool: { + max: 5, + min: 0, + idleTimeoutMillis: 30000, + }, + }; + + const pool = await sql.connect(config); + return pool; + } catch (error) { + throw new Error( + `MSSQL ์—ฐ๊ฒฐ ์‹คํŒจ: mssql ํŒจํ‚ค์ง€๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. (${error})` + ); + } +} + +/** + * Oracle ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ + */ +async function createOraclePool( + connection: ExternalDbConnection +): Promise { + try { + // oracledb๋ฅผ ๋™์ ์œผ๋กœ import + const oracledb = require("oracledb"); + + // Oracle ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” (์ตœ์ดˆ 1ํšŒ๋งŒ) + if (!oracledb.oracleClientVersion) { + // Instant Client ๊ฒฝ๋กœ ์„ค์ • (ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ง€์ • ๊ฐ€๋Šฅ) + const instantClientPath = process.env.ORACLE_INSTANT_CLIENT_PATH; + if (instantClientPath) { + oracledb.initOracleClient({ libDir: instantClientPath }); + } + } + + // ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด ์ƒ์„ฑ + const connectString = connection.database.includes("/") + ? connection.database // ์ด๋ฏธ ์ „์ฒด ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ + : `${connection.host}:${connection.port}/${connection.database}`; + + const pool = await oracledb.createPool({ + user: connection.username, + password: connection.password, + connectString: connectString, + poolMin: 1, + poolMax: 5, + poolIncrement: 1, + poolTimeout: 60, // 60์ดˆ ํ›„ ์œ ํœด ์—ฐ๊ฒฐ ํ•ด์ œ + queueTimeout: 5000, // ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ํƒ€์ž„์•„์›ƒ 5์ดˆ + enableStatistics: true, + }); + + return pool; + } catch (error: any) { + throw new Error( + `Oracle ์—ฐ๊ฒฐ ์‹คํŒจ: ${error.message}. oracledb ํŒจํ‚ค์ง€์™€ Oracle Instant Client๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.` + ); + } +} + +/** + * ํ’€์—์„œ ์ฟผ๋ฆฌ ์‹คํ–‰ (DB ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ) + */ +async function executePoolQuery( + pool: any, + dbType: string, + query: string, + params: any[] +): Promise { + const normalizedType = dbType.toLowerCase(); + + switch (normalizedType) { + case "postgresql": { + const result = await pool.query(query, params); + return { rows: result.rows, rowCount: result.rowCount }; + } + + case "mysql": + case "mariadb": { + const [rows] = await pool.query(query, params); + return { + rows: Array.isArray(rows) ? rows : [rows], + rowCount: rows.length, + }; + } + + case "mssql": { + const request = pool.request(); + // MSSQL์€ ๋ช…๋ช…๋œ ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ + params.forEach((param, index) => { + request.input(`p${index + 1}`, param); + }); + const result = await request.query(query); + return { rows: result.recordset, rowCount: result.rowCount }; + } + + case "oracle": { + const oracledb = require("oracledb"); + const connection = await pool.getConnection(); + try { + // Oracle์€ :1, :2 ํ˜•์‹์˜ ๋ฐ”์ธ๋“œ ๋ณ€์ˆ˜ ์‚ฌ์šฉ + const result = await connection.execute(query, params, { + autoCommit: false, // ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด false + outFormat: oracledb.OUT_FORMAT_OBJECT, // ๊ฐ์ฒด ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ + }); + return { rows: result.rows || [], rowCount: result.rowCount || 0 }; + } finally { + await connection.close(); + } + } + + default: + throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” DB ํƒ€์ž…: ${dbType}`); + } +} + +/** + * ์—ฐ๊ฒฐ ํ’€ ์ข…๋ฃŒ (DB ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ) + */ +async function closePool(pool: any, dbType: string): Promise { + const normalizedType = dbType.toLowerCase(); + + try { + switch (normalizedType) { + case "postgresql": + case "mysql": + case "mariadb": + await pool.end(); + break; + + case "mssql": + case "oracle": + await pool.close(); + break; + } + } catch (error) { + console.error(`ํ’€ ์ข…๋ฃŒ ์˜ค๋ฅ˜ (${dbType}):`, error); + } +} + +/** + * ์™ธ๋ถ€ DB ์ฟผ๋ฆฌ ์‹คํ–‰ + */ +export async function executeExternalQuery( + connectionId: number, + query: string, + params: any[] = [] +): Promise { + const poolInfo = await getExternalPool(connectionId); + return await executePoolQuery(poolInfo.pool, poolInfo.dbType, query, params); +} + +/** + * ์™ธ๋ถ€ DB ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ + */ +export async function executeExternalTransaction( + connectionId: number, + callback: (client: any, dbType: string) => Promise +): Promise { + const poolInfo = await getExternalPool(connectionId); + const { pool, dbType } = poolInfo; + const normalizedType = dbType.toLowerCase(); + + let client: any; + + try { + switch (normalizedType) { + case "postgresql": { + client = await pool.connect(); + await client.query(getBeginTransactionQuery(dbType)); + const result = await callback(client, dbType); + await client.query(getCommitQuery(dbType)); + return result; + } + + case "mysql": + case "mariadb": { + client = await pool.getConnection(); + await client.beginTransaction(); + const result = await callback(client, dbType); + await client.commit(); + return result; + } + + case "mssql": { + const transaction = new pool.constructor.Transaction(pool); + await transaction.begin(); + client = transaction; + const result = await callback(client, dbType); + await transaction.commit(); + return result; + } + + case "oracle": { + client = await pool.getConnection(); + // Oracle์€ ๋ช…์‹œ์  BEGIN ์—†์ด ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + const result = await callback(client, dbType); + // ๋ช…์‹œ์  ์ปค๋ฐ‹ + await client.commit(); + return result; + } + + default: + throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” DB ํƒ€์ž…: ${dbType}`); + } + } catch (error) { + console.error(`์™ธ๋ถ€ DB ํŠธ๋žœ์žญ์…˜ ์˜ค๋ฅ˜ (ID: ${connectionId}):`, error); + + // ๋กค๋ฐฑ ์‹œ๋„ + if (client) { + try { + switch (normalizedType) { + case "postgresql": + await client.query(getRollbackQuery(dbType)); + break; + + case "mysql": + case "mariadb": + await client.rollback(); + break; + + case "mssql": + case "oracle": + await client.rollback(); + break; + } + } catch (rollbackError) { + console.error("ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์˜ค๋ฅ˜:", rollbackError); + } + } + + throw error; + } finally { + // ์—ฐ๊ฒฐ ํ•ด์ œ + if (client) { + try { + switch (normalizedType) { + case "postgresql": + client.release(); + break; + + case "mysql": + case "mariadb": + client.release(); + break; + + case "oracle": + await client.close(); + break; + + case "mssql": + // MSSQL Transaction ๊ฐ์ฒด๋Š” ์ž๋™์œผ๋กœ ์ •๋ฆฌ๋จ + break; + } + } catch (releaseError) { + console.error("ํด๋ผ์ด์–ธํŠธ ํ•ด์ œ ์˜ค๋ฅ˜:", releaseError); + } + } + } +} diff --git a/backend-node/src/services/flowDataMoveService.ts b/backend-node/src/services/flowDataMoveService.ts index 9ed99548..03fd18c6 100644 --- a/backend-node/src/services/flowDataMoveService.ts +++ b/backend-node/src/services/flowDataMoveService.ts @@ -6,10 +6,25 @@ */ import db from "../database/db"; -import { FlowAuditLog, FlowIntegrationContext } from "../types/flow"; +import { + FlowAuditLog, + FlowIntegrationContext, + FlowDefinition, +} from "../types/flow"; import { FlowDefinitionService } from "./flowDefinitionService"; import { FlowStepService } from "./flowStepService"; import { FlowExternalDbIntegrationService } from "./flowExternalDbIntegrationService"; +import { + getExternalPool, + executeExternalQuery, + executeExternalTransaction, +} from "./externalDbHelper"; +import { + getPlaceholder, + buildUpdateQuery, + buildInsertQuery, + buildSelectQuery, +} from "./dbQueryBuilder"; export class FlowDataMoveService { private flowDefinitionService: FlowDefinitionService; @@ -33,6 +48,28 @@ export class FlowDataMoveService { userId: string = "system", additionalData?: Record ): Promise<{ success: boolean; targetDataId?: any; message?: string }> { + // 0. ํ”Œ๋กœ์šฐ ์ •์˜ ์กฐํšŒ (DB ์†Œ์Šค ํ™•์ธ) + const flowDefinition = await this.flowDefinitionService.findById(flowId); + if (!flowDefinition) { + throw new Error(`ํ”Œ๋กœ์šฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (ID: ${flowId})`); + } + + // ์™ธ๋ถ€ DB์ธ ๊ฒฝ์šฐ ๋ณ„๋„ ์ฒ˜๋ฆฌ + if ( + flowDefinition.dbSourceType === "external" && + flowDefinition.dbConnectionId + ) { + return await this.moveDataToStepExternal( + flowDefinition.dbConnectionId, + fromStepId, + toStepId, + dataId, + userId, + additionalData + ); + } + + // ๋‚ด๋ถ€ DB ์ฒ˜๋ฆฌ (๊ธฐ์กด ๋กœ์ง) return await db.transaction(async (client) => { try { // 1. ๋‹จ๊ณ„ ์ •๋ณด ์กฐํšŒ @@ -160,7 +197,14 @@ export class FlowDataMoveService { dataId: any, additionalData?: Record ): Promise { - const statusColumn = toStep.statusColumn || "flow_status"; + // ์ƒํƒœ ์ปฌ๋Ÿผ์ด ์ง€์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์—๋Ÿฌ + if (!toStep.statusColumn) { + throw new Error( + `๋‹จ๊ณ„ "${toStep.stepName}"์˜ ์ƒํƒœ ์ปฌ๋Ÿผ์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ”Œ๋กœ์šฐ ํŽธ์ง‘ ํ™”๋ฉด์—์„œ "์ƒํƒœ ์ปฌ๋Ÿผ๋ช…"์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.` + ); + } + + const statusColumn = toStep.statusColumn; const tableName = fromStep.tableName; // ์ถ”๊ฐ€ ํ•„๋“œ ์—…๋ฐ์ดํŠธ ์ค€๋น„ @@ -590,4 +634,307 @@ export class FlowDataMoveService { userId, ]); } + + /** + * ์™ธ๋ถ€ DB ๋ฐ์ดํ„ฐ ์ด๋™ ์ฒ˜๋ฆฌ + */ + private async moveDataToStepExternal( + dbConnectionId: number, + fromStepId: number, + toStepId: number, + dataId: any, + userId: string = "system", + additionalData?: Record + ): Promise<{ success: boolean; targetDataId?: any; message?: string }> { + return await executeExternalTransaction( + dbConnectionId, + async (externalClient, dbType) => { + try { + // 1. ๋‹จ๊ณ„ ์ •๋ณด ์กฐํšŒ (๋‚ด๋ถ€ DB์—์„œ) + const fromStep = await this.flowStepService.findById(fromStepId); + const toStep = await this.flowStepService.findById(toStepId); + + if (!fromStep || !toStep) { + throw new Error("์œ ํšจํ•˜์ง€ ์•Š์€ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค"); + } + + let targetDataId = dataId; + let sourceTable = fromStep.tableName; + let targetTable = toStep.tableName || fromStep.tableName; + + // 2. ์ด๋™ ๋ฐฉ์‹์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ + switch (toStep.moveType || "status") { + case "status": + // ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์‹ + await this.moveByStatusChangeExternal( + externalClient, + dbType, + fromStep, + toStep, + dataId, + additionalData + ); + break; + + case "table": + // ํ…Œ์ด๋ธ” ์ด๋™ ๋ฐฉ์‹ + targetDataId = await this.moveByTableTransferExternal( + externalClient, + dbType, + fromStep, + toStep, + dataId, + additionalData + ); + targetTable = toStep.targetTable || toStep.tableName; + break; + + case "both": + // ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ฐฉ์‹: ๋‘˜ ๋‹ค ์ˆ˜ํ–‰ + await this.moveByStatusChangeExternal( + externalClient, + dbType, + fromStep, + toStep, + dataId, + additionalData + ); + targetDataId = await this.moveByTableTransferExternal( + externalClient, + dbType, + fromStep, + toStep, + dataId, + additionalData + ); + targetTable = toStep.targetTable || toStep.tableName; + break; + + default: + throw new Error( + `์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ด๋™ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค: ${toStep.moveType}` + ); + } + + // 3. ์™ธ๋ถ€ ์—ฐ๋™ ์ฒ˜๋ฆฌ๋Š” ์ƒ๋žต (์™ธ๋ถ€ DB ์ž์ฒด๊ฐ€ ์™ธ๋ถ€์ด๋ฏ€๋กœ) + + // 4. ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก (๋‚ด๋ถ€ DB์—) + // ์™ธ๋ถ€ DB๋Š” ๋‚ด๋ถ€ DB ํŠธ๋žœ์žญ์…˜ ์™ธ๋ถ€์ด๋ฏ€๋กœ ์ง์ ‘ ์ฟผ๋ฆฌ ์‹คํ–‰ + const auditQuery = ` + INSERT INTO flow_audit_log ( + flow_definition_id, from_step_id, to_step_id, + 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) + `; + + await db.query(auditQuery, [ + toStep.flowDefinitionId, + fromStep.id, + toStep.id, + toStep.moveType || "status", + sourceTable, + targetTable, + dataId, + targetDataId, + null, // statusFrom + toStep.statusValue || null, // statusTo + userId, + `์™ธ๋ถ€ DB (${dbType}) ๋ฐ์ดํ„ฐ ์ด๋™`, + ]); + + return { + success: true, + targetDataId, + message: `๋ฐ์ดํ„ฐ ์ด๋™์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (์™ธ๋ถ€ DB: ${dbType})`, + }; + } catch (error: any) { + console.error("์™ธ๋ถ€ DB ๋ฐ์ดํ„ฐ ์ด๋™ ์˜ค๋ฅ˜:", error); + throw error; + } + } + ); + } + + /** + * ์™ธ๋ถ€ DB ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ด๋™ + */ + private async moveByStatusChangeExternal( + externalClient: any, + dbType: string, + fromStep: any, + toStep: any, + dataId: any, + additionalData?: Record + ): Promise { + // ์ƒํƒœ ์ปฌ๋Ÿผ์ด ์ง€์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์—๋Ÿฌ + if (!toStep.statusColumn) { + throw new Error( + `๋‹จ๊ณ„ "${toStep.stepName}"์˜ ์ƒํƒœ ์ปฌ๋Ÿผ์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ”Œ๋กœ์šฐ ํŽธ์ง‘ ํ™”๋ฉด์—์„œ "์ƒํƒœ ์ปฌ๋Ÿผ๋ช…"์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.` + ); + } + + const statusColumn = toStep.statusColumn; + const tableName = fromStep.tableName; + const normalizedDbType = dbType.toLowerCase(); + + // ์—…๋ฐ์ดํŠธํ•  ํ•„๋“œ ์ค€๋น„ + const updateFields: { column: string; value: any }[] = [ + { column: statusColumn, value: toStep.statusValue }, + ]; + + // ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + if (additionalData) { + for (const [key, value] of Object.entries(additionalData)) { + updateFields.push({ column: key, value }); + } + } + + // DB๋ณ„ ์ฟผ๋ฆฌ ์ƒ์„ฑ + const { query: updateQuery, values } = buildUpdateQuery( + dbType, + tableName, + updateFields, + "id" + ); + + // WHERE ์ ˆ ๊ฐ’ ์„ค์ • (๋งˆ์ง€๋ง‰ ํŒŒ๋ผ๋ฏธํ„ฐ) + values[values.length - 1] = dataId; + + // ์ฟผ๋ฆฌ ์‹คํ–‰ (DB ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ) + let result: any; + if (normalizedDbType === "postgresql") { + result = await externalClient.query(updateQuery, values); + } else if (normalizedDbType === "mysql" || normalizedDbType === "mariadb") { + [result] = await externalClient.query(updateQuery, values); + } else if (normalizedDbType === "mssql") { + const request = externalClient.request(); + values.forEach((val: any, idx: number) => { + request.input(`p${idx + 1}`, val); + }); + result = await request.query(updateQuery); + } else if (normalizedDbType === "oracle") { + result = await externalClient.execute(updateQuery, values, { + autoCommit: false, + }); + } + + // ๊ฒฐ๊ณผ ํ™•์ธ + const affectedRows = + normalizedDbType === "postgresql" + ? result.rowCount + : normalizedDbType === "mssql" + ? result.rowsAffected[0] + : normalizedDbType === "oracle" + ? result.rowsAffected + : result.affectedRows; + + if (affectedRows === 0) { + throw new Error(`๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${dataId}`); + } + } + + /** + * ์™ธ๋ถ€ DB ํ…Œ์ด๋ธ” ์ด๋™ ๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ ์ด๋™ + */ + private async moveByTableTransferExternal( + externalClient: any, + dbType: string, + fromStep: any, + toStep: any, + dataId: any, + additionalData?: Record + ): Promise { + const sourceTable = fromStep.tableName; + const targetTable = toStep.targetTable || toStep.tableName; + const fieldMappings = toStep.fieldMappings || {}; + const normalizedDbType = dbType.toLowerCase(); + + // 1. ์†Œ์Šค ๋ฐ์ดํ„ฐ ์กฐํšŒ + const { query: selectQuery, placeholder } = buildSelectQuery( + dbType, + sourceTable, + "id" + ); + + let sourceResult: any; + if (normalizedDbType === "postgresql") { + sourceResult = await externalClient.query(selectQuery, [dataId]); + } else if (normalizedDbType === "mysql" || normalizedDbType === "mariadb") { + [sourceResult] = await externalClient.query(selectQuery, [dataId]); + } else if (normalizedDbType === "mssql") { + const request = externalClient.request(); + request.input("p1", dataId); + sourceResult = await request.query(selectQuery); + sourceResult = { rows: sourceResult.recordset }; + } else if (normalizedDbType === "oracle") { + sourceResult = await externalClient.execute(selectQuery, [dataId], { + autoCommit: false, + outFormat: 4001, // oracledb.OUT_FORMAT_OBJECT + }); + } + + const rows = sourceResult.rows || sourceResult; + if (!rows || rows.length === 0) { + throw new Error(`์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${dataId}`); + } + + const sourceData = rows[0]; + + // 2. ํ•„๋“œ ๋งคํ•‘ ์ ์šฉ + const targetData: Record = {}; + + for (const [targetField, sourceField] of Object.entries(fieldMappings)) { + const sourceFieldKey = sourceField as string; + if (sourceData[sourceFieldKey] !== undefined) { + targetData[targetField] = sourceData[sourceFieldKey]; + } + } + + // ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ + if (additionalData) { + Object.assign(targetData, additionalData); + } + + // 3. ๋Œ€์ƒ ํ…Œ์ด๋ธ”์— ์‚ฝ์ž… + const { query: insertQuery, values } = buildInsertQuery( + dbType, + targetTable, + targetData + ); + + let insertResult: any; + let newDataId: any; + + if (normalizedDbType === "postgresql") { + insertResult = await externalClient.query(insertQuery, values); + newDataId = insertResult.rows[0].id; + } else if (normalizedDbType === "mysql" || normalizedDbType === "mariadb") { + [insertResult] = await externalClient.query(insertQuery, values); + newDataId = insertResult.insertId; + } else if (normalizedDbType === "mssql") { + const request = externalClient.request(); + values.forEach((val: any, idx: number) => { + request.input(`p${idx + 1}`, val); + }); + insertResult = await request.query(insertQuery); + newDataId = insertResult.recordset[0].id; + } else if (normalizedDbType === "oracle") { + // Oracle RETURNING ์ ˆ ์ฒ˜๋ฆฌ + const outBinds: any = { id: { dir: 3003, type: 2001 } }; // OUT, NUMBER + insertResult = await externalClient.execute(insertQuery, values, { + autoCommit: false, + outBinds: outBinds, + }); + newDataId = insertResult.outBinds.id[0]; + } + + // 4. ํ•„์š” ์‹œ ์†Œ์Šค ๋ฐ์ดํ„ฐ ์‚ญ์ œ (์˜ต์…˜) + // const deletePlaceholder = getPlaceholder(dbType, 1); + // await externalClient.query(`DELETE FROM ${sourceTable} WHERE id = ${deletePlaceholder}`, [dataId]); + + return newDataId; + } } diff --git a/backend-node/src/services/flowDefinitionService.ts b/backend-node/src/services/flowDefinitionService.ts index 859e0792..f08f934d 100644 --- a/backend-node/src/services/flowDefinitionService.ts +++ b/backend-node/src/services/flowDefinitionService.ts @@ -17,18 +17,33 @@ export class FlowDefinitionService { request: CreateFlowDefinitionRequest, userId: string ): Promise { + console.log("๐Ÿ”ฅ flowDefinitionService.create called with:", { + name: request.name, + description: request.description, + tableName: request.tableName, + dbSourceType: request.dbSourceType, + dbConnectionId: request.dbConnectionId, + userId, + }); + const query = ` - INSERT INTO flow_definition (name, description, table_name, created_by) - VALUES ($1, $2, $3, $4) + INSERT INTO flow_definition (name, description, table_name, db_source_type, db_connection_id, created_by) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `; - const result = await db.query(query, [ + const values = [ request.name, request.description || null, - request.tableName, + request.tableName || null, + request.dbSourceType || "internal", + request.dbConnectionId || null, userId, - ]); + ]; + + console.log("๐Ÿ’พ Executing INSERT with values:", values); + + const result = await db.query(query, values); return this.mapToFlowDefinition(result[0]); } @@ -162,6 +177,8 @@ export class FlowDefinitionService { name: row.name, description: row.description, tableName: row.table_name, + dbSourceType: row.db_source_type || "internal", + dbConnectionId: row.db_connection_id, isActive: row.is_active, createdBy: row.created_by, createdAt: row.created_at, diff --git a/backend-node/src/services/flowExecutionService.ts b/backend-node/src/services/flowExecutionService.ts index ae4f1369..9d9eb9c4 100644 --- a/backend-node/src/services/flowExecutionService.ts +++ b/backend-node/src/services/flowExecutionService.ts @@ -8,6 +8,8 @@ import { FlowStepDataCount, FlowStepDataList } from "../types/flow"; import { FlowDefinitionService } from "./flowDefinitionService"; import { FlowStepService } from "./flowStepService"; import { FlowConditionParser } from "./flowConditionParser"; +import { executeExternalQuery } from "./externalDbHelper"; +import { getPlaceholder, buildPaginationClause } from "./dbQueryBuilder"; export class FlowExecutionService { private flowDefinitionService: FlowDefinitionService; @@ -28,6 +30,13 @@ export class FlowExecutionService { throw new Error(`Flow definition not found: ${flowId}`); } + console.log("๐Ÿ” [getStepDataCount] Flow Definition:", { + flowId, + dbSourceType: flowDef.dbSourceType, + dbConnectionId: flowDef.dbConnectionId, + tableName: flowDef.tableName, + }); + // 2. ํ”Œ๋กœ์šฐ ๋‹จ๊ณ„ ์กฐํšŒ const step = await this.flowStepService.findById(stepId); if (!step) { @@ -46,11 +55,40 @@ export class FlowExecutionService { step.conditionJson ); - // 5. ์นด์šดํŠธ ์ฟผ๋ฆฌ ์‹คํ–‰ + // 5. ์นด์šดํŠธ ์ฟผ๋ฆฌ ์‹คํ–‰ (๋‚ด๋ถ€ ๋˜๋Š” ์™ธ๋ถ€ DB) const query = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`; - const result = await db.query(query, params); - return parseInt(result[0].count); + console.log("๐Ÿ” [getStepDataCount] Query Info:", { + tableName, + query, + params, + isExternal: flowDef.dbSourceType === "external", + connectionId: flowDef.dbConnectionId, + }); + + let result: any; + if (flowDef.dbSourceType === "external" && flowDef.dbConnectionId) { + // ์™ธ๋ถ€ DB ์กฐํšŒ + console.log( + "โœ… [getStepDataCount] Using EXTERNAL DB:", + flowDef.dbConnectionId + ); + const externalResult = await executeExternalQuery( + flowDef.dbConnectionId, + query, + params + ); + console.log("๐Ÿ“ฆ [getStepDataCount] External result:", externalResult); + result = externalResult.rows; + } else { + // ๋‚ด๋ถ€ DB ์กฐํšŒ + console.log("โœ… [getStepDataCount] Using INTERNAL DB"); + result = await db.query(query, params); + } + + const count = parseInt(result[0].count || result[0].COUNT); + console.log("โœ… [getStepDataCount] Final count:", count); + return count; } /** @@ -88,47 +126,98 @@ export class FlowExecutionService { const offset = (page - 1) * pageSize; + const isExternalDb = + flowDef.dbSourceType === "external" && flowDef.dbConnectionId; + // 5. ์ „์ฒด ์นด์šดํŠธ const countQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`; - const countResult = await db.query(countQuery, params); - const total = parseInt(countResult[0].count); + let countResult: any; + let total: number; - // 6. ํ…Œ์ด๋ธ”์˜ Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ - let orderByColumn = ""; - try { - const pkQuery = ` - SELECT a.attname - FROM pg_index i - JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = $1::regclass - AND i.indisprimary - LIMIT 1 - `; - const pkResult = await db.query(pkQuery, [tableName]); - if (pkResult.length > 0) { - orderByColumn = pkResult[0].attname; - } - } catch (err) { - // Primary Key๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด ORDER BY ์—†์ด ์ง„ํ–‰ - console.warn(`Could not find primary key for table ${tableName}:`, err); + if (isExternalDb) { + const externalCountResult = await executeExternalQuery( + flowDef.dbConnectionId!, + countQuery, + params + ); + countResult = externalCountResult.rows; + total = parseInt(countResult[0].count || countResult[0].COUNT); + } else { + countResult = await db.query(countQuery, params); + total = parseInt(countResult[0].count); } - // 7. ๋ฐ์ดํ„ฐ ์กฐํšŒ - const orderByClause = orderByColumn ? `ORDER BY ${orderByColumn} DESC` : ""; - const dataQuery = ` - SELECT * FROM ${tableName} - WHERE ${where} - ${orderByClause} - LIMIT $${params.length + 1} OFFSET $${params.length + 2} - `; - const dataResult = await db.query(dataQuery, [...params, pageSize, offset]); + // 6. ๋ฐ์ดํ„ฐ ์กฐํšŒ (DB ํƒ€์ž…๋ณ„ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ) + let dataQuery: string; + let dataParams: any[]; - return { - records: dataResult, - total, - page, - pageSize, - }; + if (isExternalDb) { + // ์™ธ๋ถ€ DB๋Š” id ์ปฌ๋Ÿผ์œผ๋กœ ์ •๋ ฌ (๊ฐ€์ •) + // DB ํƒ€์ž…์— ๋”ฐ๋ฅธ ํŽ˜์ด์ง• ์ ˆ์€ ๋นŒ๋”์—์„œ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ์ž‘์„ฑ + // PostgreSQL, MySQL, MSSQL, Oracle ๋ชจ๋‘ ์ง€์›ํ•˜๋„๋ก ๋‹จ์ˆœํ™” + dataQuery = ` + SELECT * FROM ${tableName} + WHERE ${where} + ORDER BY id DESC + LIMIT ${pageSize} OFFSET ${offset} + `; + dataParams = params; + + const externalDataResult = await executeExternalQuery( + flowDef.dbConnectionId!, + dataQuery, + dataParams + ); + + return { + records: externalDataResult.rows, + total, + page, + pageSize, + }; + } else { + // ๋‚ด๋ถ€ DB (PostgreSQL) + // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + let orderByColumn = ""; + try { + const pkQuery = ` + SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = $1::regclass + AND i.indisprimary + LIMIT 1 + `; + const pkResult = await db.query(pkQuery, [tableName]); + if (pkResult.length > 0) { + orderByColumn = pkResult[0].attname; + } + } catch (err) { + console.warn(`Could not find primary key for table ${tableName}:`, err); + } + + const orderByClause = orderByColumn + ? `ORDER BY ${orderByColumn} DESC` + : ""; + dataQuery = ` + SELECT * FROM ${tableName} + WHERE ${where} + ${orderByClause} + LIMIT $${params.length + 1} OFFSET $${params.length + 2} + `; + const dataResult = await db.query(dataQuery, [ + ...params, + pageSize, + offset, + ]); + + return { + records: dataResult, + total, + page, + pageSize, + }; + } } /** diff --git a/backend-node/src/types/flow.ts b/backend-node/src/types/flow.ts index 3483b617..0c84fbeb 100644 --- a/backend-node/src/types/flow.ts +++ b/backend-node/src/types/flow.ts @@ -8,6 +8,8 @@ export interface FlowDefinition { name: string; description?: string; tableName: string; + dbSourceType?: "internal" | "external"; // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†Œ์Šค ํƒ€์ž… + dbConnectionId?: number; // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ID (external์ธ ๊ฒฝ์šฐ) isActive: boolean; createdBy?: string; createdAt: Date; @@ -19,6 +21,8 @@ export interface CreateFlowDefinitionRequest { name: string; description?: string; tableName: string; + dbSourceType?: "internal" | "external"; // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†Œ์Šค ํƒ€์ž… + dbConnectionId?: number; // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ID } // ํ”Œ๋กœ์šฐ ์ •์˜ ์ˆ˜์ • ์š”์ฒญ diff --git a/frontend/app/(main)/admin/flow-management/[id]/page.tsx b/frontend/app/(main)/admin/flow-management/[id]/page.tsx index 77d42718..a311bc63 100644 --- a/frontend/app/(main)/admin/flow-management/[id]/page.tsx +++ b/frontend/app/(main)/admin/flow-management/[id]/page.tsx @@ -73,7 +73,9 @@ export default function FlowEditorPage() { // ํ”Œ๋กœ์šฐ ์ •์˜ ๋กœ๋“œ const flowRes = await getFlowDefinition(flowId); if (flowRes.success && flowRes.data) { - setFlowDefinition(flowRes.data); + console.log("๐Ÿ” Flow Definition loaded:", flowRes.data); + console.log("๐Ÿ“‹ Table Name:", flowRes.data.definition?.tableName); + setFlowDefinition(flowRes.data.definition); } // ๋‹จ๊ณ„ ๋กœ๋“œ @@ -314,6 +316,9 @@ export default function FlowEditorPage() { setSelectedStep(null)} onUpdate={loadFlowData} /> diff --git a/frontend/app/(main)/admin/flow-management/page.tsx b/frontend/app/(main)/admin/flow-management/page.tsx index 999fb6fa..f36bd5a2 100644 --- a/frontend/app/(main)/admin/flow-management/page.tsx +++ b/frontend/app/(main)/admin/flow-management/page.tsx @@ -9,7 +9,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User } from "lucide-react"; +import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User, Check, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -27,6 +27,11 @@ import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { getFlowDefinitions, createFlowDefinition, deleteFlowDefinition } from "@/lib/api/flow"; import { FlowDefinition } from "@/types/flow"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { tableManagementApi } from "@/lib/api/tableManagement"; export default function FlowManagementPage() { const router = useRouter(); @@ -39,6 +44,15 @@ export default function FlowManagementPage() { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedFlow, setSelectedFlow] = useState(null); + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๊ด€๋ จ ์ƒํƒœ + const [tableList, setTableList] = useState([]); // ๋‚ด๋ถ€ DB ํ…Œ์ด๋ธ” + const [loadingTables, setLoadingTables] = useState(false); + const [openTableCombobox, setOpenTableCombobox] = useState(false); + const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" ๋˜๋Š” ์™ธ๋ถ€ DB connection ID + const [externalConnections, setExternalConnections] = useState([]); + const [externalTableList, setExternalTableList] = useState([]); + const [loadingExternalTables, setLoadingExternalTables] = useState(false); + // ์ƒ์„ฑ ํผ ์ƒํƒœ const [formData, setFormData] = useState({ name: "", @@ -75,9 +89,107 @@ export default function FlowManagementPage() { loadFlows(); }, []); + // ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ (๋‚ด๋ถ€ DB) + useEffect(() => { + const loadTables = async () => { + try { + setLoadingTables(true); + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setTableList(response.data); + } + } catch (error) { + console.error("Failed to load tables:", error); + } finally { + setLoadingTables(false); + } + }; + loadTables(); + }, []); + + // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + const loadConnections = async () => { + try { + const token = localStorage.getItem("authToken"); + if (!token) { + console.warn("No auth token found"); + return; + } + + const response = await fetch("/api/external-db-connections/control/active", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response && response.ok) { + const data = await response.json(); + if (data.success && data.data) { + // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(ํ˜„์žฌ ์‹œ์Šคํ…œ) ์ œ์™ธ - connection_name์— "๋ฉ”์ธ" ๋˜๋Š” "ํ˜„์žฌ ์‹œ์Šคํ…œ"์ด ํฌํ•จ๋œ ๊ฒƒ ํ•„ํ„ฐ๋ง + const filtered = data.data.filter( + (conn: any) => !conn.connection_name.includes("๋ฉ”์ธ") && !conn.connection_name.includes("ํ˜„์žฌ ์‹œ์Šคํ…œ"), + ); + setExternalConnections(filtered); + } + } + } catch (error) { + console.error("Failed to load external connections:", error); + setExternalConnections([]); + } + }; + loadConnections(); + }, []); + + // ์™ธ๋ถ€ DB ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + if (selectedDbSource === "internal" || !selectedDbSource) { + setExternalTableList([]); + return; + } + + const loadExternalTables = async () => { + try { + setLoadingExternalTables(true); + const token = localStorage.getItem("authToken"); + + const response = await fetch(`/api/multi-connection/connections/${selectedDbSource}/tables`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response && response.ok) { + const data = await response.json(); + if (data.success && data.data) { + const tables = Array.isArray(data.data) ? data.data : []; + const tableNames = tables + .map((t: any) => (typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name)) + .filter(Boolean); + setExternalTableList(tableNames); + } else { + setExternalTableList([]); + } + } else { + setExternalTableList([]); + } + } catch (error) { + console.error("์™ธ๋ถ€ DB ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ ์˜ค๋ฅ˜:", error); + setExternalTableList([]); + } finally { + setLoadingExternalTables(false); + } + }; + + loadExternalTables(); + }, [selectedDbSource]); + // ํ”Œ๋กœ์šฐ ์ƒ์„ฑ const handleCreate = async () => { + console.log("๐Ÿš€ handleCreate called with formData:", formData); + if (!formData.name || !formData.tableName) { + console.log("โŒ Validation failed:", { name: formData.name, tableName: formData.tableName }); toast({ title: "์ž…๋ ฅ ์˜ค๋ฅ˜", description: "ํ”Œ๋กœ์šฐ ์ด๋ฆ„๊ณผ ํ…Œ์ด๋ธ” ์ด๋ฆ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", @@ -87,7 +199,15 @@ export default function FlowManagementPage() { } try { - const response = await createFlowDefinition(formData); + // DB ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ + const requestData = { + ...formData, + dbSourceType: selectedDbSource === "internal" ? "internal" : "external", + dbConnectionId: selectedDbSource === "internal" ? undefined : Number(selectedDbSource), + }; + + console.log("โœ… Calling createFlowDefinition with:", requestData); + const response = await createFlowDefinition(requestData); if (response.success && response.data) { toast({ title: "์ƒ์„ฑ ์™„๋ฃŒ", @@ -95,6 +215,7 @@ export default function FlowManagementPage() { }); setIsCreateDialogOpen(false); setFormData({ name: "", description: "", tableName: "" }); + setSelectedDbSource("internal"); loadFlows(); } else { toast({ @@ -277,19 +398,123 @@ export default function FlowManagementPage() { /> + {/* DB ์†Œ์Šค ์„ ํƒ */} +
+ + +

+ ํ”Œ๋กœ์šฐ์—์„œ ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค +

+
+ + {/* ํ…Œ์ด๋ธ” ์„ ํƒ */}
- setFormData({ ...formData, tableName: e.target.value })} - placeholder="์˜ˆ: products" - className="h-8 text-xs sm:h-10 sm:text-sm" - /> + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {selectedDbSource === "internal" + ? // ๋‚ด๋ถ€ DB ํ…Œ์ด๋ธ” ๋ชฉ๋ก + tableList.map((table) => ( + { + console.log("๐Ÿ“ Internal table selected:", { + tableName: table.tableName, + currentValue, + }); + setFormData({ ...formData, tableName: currentValue }); + setOpenTableCombobox(false); + }} + className="text-xs sm:text-sm" + > + +
+ {table.displayName || table.tableName} + {table.description && ( + {table.description} + )} +
+
+ )) + : // ์™ธ๋ถ€ DB ํ…Œ์ด๋ธ” ๋ชฉ๋ก + externalTableList.map((tableName, index) => ( + { + setFormData({ ...formData, tableName: currentValue }); + setOpenTableCombobox(false); + }} + className="text-xs sm:text-sm" + > + +
{tableName}
+
+ ))} +
+
+
+
+

- ํ”Œ๋กœ์šฐ๊ฐ€ ๊ด€๋ฆฌํ•  ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š” + ํ”Œ๋กœ์šฐ์˜ ๋ชจ๋“  ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•  ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค (๋‹จ๊ณ„๋งˆ๋‹ค ์ƒํƒœ ์ปฌ๋Ÿผ๋งŒ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค)

diff --git a/frontend/app/(main)/main/page.tsx b/frontend/app/(main)/main/page.tsx index 0c6eb73e..3784fb06 100644 --- a/frontend/app/(main)/main/page.tsx +++ b/frontend/app/(main)/main/page.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge"; */ export default function MainPage() { return ( -
+
{/* ๋ฉ”์ธ ์ปจํ…์ธ  */} {/* Welcome Message */} @@ -18,7 +18,7 @@ export default function MainPage() {

Vexolor์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!

์ œํ’ˆ ์ˆ˜๋ช… ์ฃผ๊ธฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ํšจ์œจ์ ์ธ ์—…๋ฌด๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”.

- Spring Boot + Node.js Next.js Shadcn/ui
diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index ccc4bba3..88f98b84 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -1,15 +1,12 @@ export default function MainHomePage() { return ( -
+
{/* ๋Œ€์‹œ๋ณด๋“œ ์ปจํ…์ธ  */}

WACE ์†”๋ฃจ์…˜์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!

์ œํ’ˆ ์ˆ˜๋ช… ์ฃผ๊ธฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ํšจ์œจ์ ์ธ ์—…๋ฌด๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”.

- - Spring Boot - Next.js diff --git a/frontend/components/flow/FlowConditionBuilder.tsx b/frontend/components/flow/FlowConditionBuilder.tsx index f6421eba..c3572dc8 100644 --- a/frontend/components/flow/FlowConditionBuilder.tsx +++ b/frontend/components/flow/FlowConditionBuilder.tsx @@ -16,6 +16,8 @@ import { getTableColumns } from "@/lib/api/tableManagement"; interface FlowConditionBuilderProps { flowId: number; tableName?: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… + dbSourceType?: "internal" | "external"; // DB ์†Œ์Šค ํƒ€์ž… + dbConnectionId?: number; // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ID condition?: FlowConditionGroup; onChange: (condition: FlowConditionGroup | undefined) => void; } @@ -35,7 +37,14 @@ const OPERATORS: { value: ConditionOperator; label: string }[] = [ { value: "is_not_null", label: "NOT NULL" }, ]; -export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: FlowConditionBuilderProps) { +export function FlowConditionBuilder({ + flowId, + tableName, + dbSourceType = "internal", + dbConnectionId, + condition, + onChange, +}: FlowConditionBuilderProps) { const [columns, setColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [conditionType, setConditionType] = useState<"AND" | "OR">(condition?.type || "AND"); @@ -52,7 +61,7 @@ export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: } }, [condition]); - // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ - ๋‚ด๋ถ€/์™ธ๋ถ€ DB ๋ชจ๋‘ ์ง€์› useEffect(() => { if (!tableName) { setColumns([]); @@ -62,17 +71,69 @@ export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: const loadColumns = async () => { try { setLoadingColumns(true); - console.log("๐Ÿ” Loading columns for table:", tableName); - const response = await getTableColumns(tableName); - console.log("๐Ÿ“ฆ Column API response:", response); + console.log("๐Ÿ” [FlowConditionBuilder] Loading columns:", { + tableName, + dbSourceType, + dbConnectionId, + }); - if (response.success && response.data?.columns) { - const columnArray = Array.isArray(response.data.columns) ? response.data.columns : []; - console.log("โœ… Setting columns:", columnArray.length, "items"); - setColumns(columnArray); + // ์™ธ๋ถ€ DB์ธ ๊ฒฝ์šฐ + if (dbSourceType === "external" && dbConnectionId) { + const token = localStorage.getItem("authToken"); + if (!token) { + console.warn("ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + setColumns([]); + return; + } + + const response = await fetch( + `/api/multi-connection/connections/${dbConnectionId}/tables/${tableName}/columns`, + { + credentials: "include", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ).catch((err) => { + console.warn("์™ธ๋ถ€ DB ์ปฌ๋Ÿผ fetch ์‹คํŒจ:", err); + return null; + }); + + if (response && response.ok) { + const result = await response.json(); + console.log("โœ… [FlowConditionBuilder] External columns response:", result); + + if (result.success && result.data) { + const columnList = Array.isArray(result.data) + ? result.data.map((col: any) => ({ + column_name: col.column_name || col.columnName || col.name, + data_type: col.data_type || col.dataType || col.type, + })) + : []; + console.log("โœ… Setting external columns:", columnList.length, "items"); + setColumns(columnList); + } else { + console.warn("โŒ No data in external columns response"); + setColumns([]); + } + } else { + console.warn(`์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ: ${response?.status}`); + setColumns([]); + } } else { - console.error("โŒ Failed to load columns:", response.message); - setColumns([]); + // ๋‚ด๋ถ€ DB์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) + const response = await getTableColumns(tableName); + console.log("๐Ÿ“ฆ [FlowConditionBuilder] Internal columns response:", response); + + if (response.success && response.data?.columns) { + const columnArray = Array.isArray(response.data.columns) ? response.data.columns : []; + console.log("โœ… Setting internal columns:", columnArray.length, "items"); + setColumns(columnArray); + } else { + console.error("โŒ Failed to load internal columns:", response.message); + setColumns([]); + } } } catch (error) { console.error("โŒ Exception loading columns:", error); @@ -83,7 +144,7 @@ export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: }; loadColumns(); - }, [tableName]); + }, [tableName, dbSourceType, dbConnectionId]); // ์กฐ๊ฑด ๋ณ€๊ฒฝ ์‹œ ๋ถ€๋ชจ์— ์ „๋‹ฌ useEffect(() => { diff --git a/frontend/components/flow/FlowStepPanel.tsx b/frontend/components/flow/FlowStepPanel.tsx index 013a52aa..21a660bb 100644 --- a/frontend/components/flow/FlowStepPanel.tsx +++ b/frontend/components/flow/FlowStepPanel.tsx @@ -31,16 +31,35 @@ import { Textarea } from "@/components/ui/textarea"; interface FlowStepPanelProps { step: FlowStep; flowId: number; + flowTableName?: string; // ํ”Œ๋กœ์šฐ ์ •์˜์—์„œ ์„ ํƒํ•œ ํ…Œ์ด๋ธ”๋ช… + flowDbSourceType?: "internal" | "external"; // ํ”Œ๋กœ์šฐ์˜ DB ์†Œ์Šค ํƒ€์ž… + flowDbConnectionId?: number; // ํ”Œ๋กœ์šฐ์˜ ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ID onClose: () => void; onUpdate: () => void; } -export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanelProps) { +export function FlowStepPanel({ + step, + flowId, + flowTableName, + flowDbSourceType = "internal", + flowDbConnectionId, + onClose, + onUpdate, +}: FlowStepPanelProps) { const { toast } = useToast(); + console.log("๐ŸŽฏ FlowStepPanel Props:", { + stepTableName: step.tableName, + flowTableName, + flowDbSourceType, + flowDbConnectionId, + final: step.tableName || flowTableName || "", + }); + const [formData, setFormData] = useState({ stepName: step.stepName, - tableName: step.tableName || "", + tableName: step.tableName || flowTableName || "", // ํ”Œ๋กœ์šฐ ํ…Œ์ด๋ธ”๋ช… ์šฐ์„  ์‚ฌ์šฉ (์‹ ๊ทœ ๋ฐฉ์‹) conditionJson: step.conditionJson, // ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋“œ ํ•„๋“œ moveType: step.moveType || "status", @@ -215,11 +234,12 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel stepName: step.stepName, statusColumn: step.statusColumn, statusValue: step.statusValue, + flowTableName, // ํ”Œ๋กœ์šฐ ์ •์˜์˜ ํ…Œ์ด๋ธ”๋ช… }); const newFormData = { stepName: step.stepName, - tableName: step.tableName || "", + tableName: step.tableName || flowTableName || "", // ํ”Œ๋กœ์šฐ ํ…Œ์ด๋ธ”๋ช… ์šฐ์„  ์‚ฌ์šฉ conditionJson: step.conditionJson, // ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ชจ๋“œ ํ•„๋“œ moveType: step.moveType || "status", @@ -234,9 +254,9 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel console.log("โœ… Setting formData:", newFormData); setFormData(newFormData); - }, [step.id]); // step ์ „์ฒด๊ฐ€ ์•„๋‹Œ step.id๋งŒ ์˜์กด์„ฑ์œผ๋กœ ์„ค์ • + }, [step.id, flowTableName]); // flowTableName๋„ ์˜์กด์„ฑ ์ถ”๊ฐ€ - // ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ + // ํ…Œ์ด๋ธ” ์„ ํƒ ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ - ๋‚ด๋ถ€/์™ธ๋ถ€ DB ๋ชจ๋‘ ์ง€์› useEffect(() => { const loadColumns = async () => { if (!formData.tableName) { @@ -246,16 +266,70 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel try { setLoadingColumns(true); - console.log("๐Ÿ” Loading columns for status column selector:", formData.tableName); - const response = await getTableColumns(formData.tableName); - console.log("๐Ÿ“ฆ Columns response:", response); + console.log("๐Ÿ” Loading columns for status column selector:", { + tableName: formData.tableName, + flowDbSourceType, + flowDbConnectionId, + }); - if (response.success && response.data && response.data.columns) { - console.log("โœ… Setting columns:", response.data.columns); - setColumns(response.data.columns); + // ์™ธ๋ถ€ DB์ธ ๊ฒฝ์šฐ + if (flowDbSourceType === "external" && flowDbConnectionId) { + const token = localStorage.getItem("authToken"); + if (!token) { + console.warn("ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + setColumns([]); + return; + } + + // ์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ์กฐํšŒ API + const response = await fetch( + `/api/multi-connection/connections/${flowDbConnectionId}/tables/${formData.tableName}/columns`, + { + credentials: "include", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ).catch((err) => { + console.warn("์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ๋ชฉ๋ก fetch ์‹คํŒจ:", err); + return null; + }); + + if (response && response.ok) { + const result = await response.json(); + console.log("โœ… External columns API response:", result); + + if (result.success && result.data) { + // ์ปฌ๋Ÿผ ๋ฐ์ดํ„ฐ ํ˜•์‹ ํ†ต์ผ + const columnList = Array.isArray(result.data) + ? result.data.map((col: any) => ({ + column_name: col.column_name || col.columnName || col.name, + data_type: col.data_type || col.dataType || col.type, + })) + : []; + console.log("โœ… Setting external columns:", columnList); + setColumns(columnList); + } else { + console.warn("โŒ No data in external columns response"); + setColumns([]); + } + } else { + console.warn(`์™ธ๋ถ€ DB ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: ${response?.status || "๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜"}`); + setColumns([]); + } } else { - console.log("โŒ No columns in response"); - setColumns([]); + // ๋‚ด๋ถ€ DB์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) + const response = await getTableColumns(formData.tableName); + console.log("๐Ÿ“ฆ Internal columns response:", response); + + if (response.success && response.data && response.data.columns) { + console.log("โœ… Setting internal columns:", response.data.columns); + setColumns(response.data.columns); + } else { + console.log("โŒ No columns in response"); + setColumns([]); + } } } catch (error) { console.error("Failed to load columns:", error); @@ -266,7 +340,7 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel }; loadColumns(); - }, [formData.tableName]); + }, [formData.tableName, flowDbSourceType, flowDbConnectionId]); // formData์˜ ์ตœ์‹  ๊ฐ’์„ ํ•ญ์ƒ ์ฐธ์กฐํ•˜๊ธฐ ์œ„ํ•œ ref const formDataRef = useRef(formData); @@ -280,6 +354,27 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel const handleSave = useCallback(async () => { const currentFormData = formDataRef.current; console.log("๐Ÿš€ handleSave called, formData:", JSON.stringify(currentFormData, null, 2)); + + // ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์‹์ผ ๋•Œ ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ + if (currentFormData.moveType === "status") { + if (!currentFormData.statusColumn) { + toast({ + title: "์ž…๋ ฅ ์˜ค๋ฅ˜", + description: "์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด '์ƒํƒœ ์ปฌ๋Ÿผ๋ช…'์„ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + if (!currentFormData.statusValue) { + toast({ + title: "์ž…๋ ฅ ์˜ค๋ฅ˜", + description: "์ƒํƒœ ๋ณ€๊ฒฝ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด '์ด ๋‹จ๊ณ„์˜ ์ƒํƒœ๊ฐ’'์„ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + } + try { const response = await updateFlowStep(step.id, currentFormData); console.log("๐Ÿ“ก API response:", response); @@ -368,8 +463,9 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
+ {/* ===== ๊ตฌ๋ฒ„์ „: ๋‹จ๊ณ„๋ณ„ ํ…Œ์ด๋ธ” ์„ ํƒ ๋ฐฉ์‹ (์ฃผ์„์ฒ˜๋ฆฌ) ===== */} {/* DB ์†Œ์Šค ์„ ํƒ */} -
+ {/*

์กฐํšŒํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค

-
+
*/} {/* ํ…Œ์ด๋ธ” ์„ ํƒ */} -
+ {/*
@@ -478,7 +574,16 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel ? "์ด ๋‹จ๊ณ„์—์„œ ์กฐ๊ฑด์„ ์ ์šฉํ•  ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค" : "์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค"}

+
*/} + {/* ===== ๊ตฌ๋ฒ„์ „ ๋ ===== */} + + {/* ===== ์‹ ๋ฒ„์ „: ํ”Œ๋กœ์šฐ์—์„œ ์„ ํƒํ•œ ํ…Œ์ด๋ธ” ํ‘œ์‹œ๋งŒ ===== */} +
+ + +

ํ”Œ๋กœ์šฐ ์ƒ์„ฑ ์‹œ ์„ ํƒํ•œ ํ…Œ์ด๋ธ”์ž…๋‹ˆ๋‹ค (์ˆ˜์ • ๋ถˆ๊ฐ€)

+ {/* ===== ์‹ ๋ฒ„์ „ ๋ ===== */} @@ -495,6 +600,8 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel setFormData({ ...formData, conditionJson: condition })} />