Compare commits
No commits in common. "641a1712703bd16e0a667f40aa96bd06f2e451ec" and "45d00e10e7a400eb9d7b8ef5b4dc7a5d92627ee8" have entirely different histories.
641a171270
...
45d00e10e7
|
|
@ -34,31 +34,23 @@ export class FlowController {
|
||||||
const { name, description, tableName } = req.body;
|
const { name, description, tableName } = req.body;
|
||||||
const userId = (req as any).user?.userId || "system";
|
const userId = (req as any).user?.userId || "system";
|
||||||
|
|
||||||
console.log("🔍 createFlowDefinition called with:", {
|
if (!name || !tableName) {
|
||||||
name,
|
|
||||||
description,
|
|
||||||
tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Name is required",
|
message: "Name and tableName are required",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테이블 이름이 제공된 경우에만 존재 확인
|
// 테이블 존재 확인
|
||||||
if (tableName) {
|
const tableExists =
|
||||||
const tableExists =
|
await this.flowDefinitionService.checkTableExists(tableName);
|
||||||
await this.flowDefinitionService.checkTableExists(tableName);
|
if (!tableExists) {
|
||||||
if (!tableExists) {
|
res.status(400).json({
|
||||||
res.status(400).json({
|
success: false,
|
||||||
success: false,
|
message: `Table '${tableName}' does not exist`,
|
||||||
message: `Table '${tableName}' does not exist`,
|
});
|
||||||
});
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowDef = await this.flowDefinitionService.create(
|
const flowDef = await this.flowDefinitionService.create(
|
||||||
|
|
@ -302,13 +294,6 @@ export class FlowController {
|
||||||
color,
|
color,
|
||||||
positionX,
|
positionX,
|
||||||
positionY,
|
positionY,
|
||||||
moveType,
|
|
||||||
statusColumn,
|
|
||||||
statusValue,
|
|
||||||
targetTable,
|
|
||||||
fieldMappings,
|
|
||||||
integrationType,
|
|
||||||
integrationConfig,
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const step = await this.flowStepService.update(id, {
|
const step = await this.flowStepService.update(id, {
|
||||||
|
|
@ -319,13 +304,6 @@ export class FlowController {
|
||||||
color,
|
color,
|
||||||
positionX,
|
positionX,
|
||||||
positionY,
|
positionY,
|
||||||
moveType,
|
|
||||||
statusColumn,
|
|
||||||
statusValue,
|
|
||||||
targetTable,
|
|
||||||
fieldMappings,
|
|
||||||
integrationType,
|
|
||||||
integrationConfig,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!step) {
|
if (!step) {
|
||||||
|
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
/**
|
|
||||||
* 데이터베이스별 쿼리 빌더
|
|
||||||
* 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<string, any>
|
|
||||||
): { 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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,477 +0,0 @@
|
||||||
/**
|
|
||||||
* 외부 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<number, any>();
|
|
||||||
|
|
||||||
// 비밀번호 복호화 유틸
|
|
||||||
const credentialEncryption = new CredentialEncryption(
|
|
||||||
process.env.ENCRYPTION_SECRET_KEY || "default-secret-key-change-in-production"
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 DB 연결 정보 조회
|
|
||||||
*/
|
|
||||||
async function getExternalConnection(
|
|
||||||
connectionId: number
|
|
||||||
): Promise<ExternalDbConnection | null> {
|
|
||||||
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<any> {
|
|
||||||
// 캐시된 연결 풀 확인
|
|
||||||
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<PgPool> {
|
|
||||||
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<mysql.Pool> {
|
|
||||||
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<any> {
|
|
||||||
// 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<any> {
|
|
||||||
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<any> {
|
|
||||||
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<void> {
|
|
||||||
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<any> {
|
|
||||||
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<any>
|
|
||||||
): Promise<any> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,25 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import db from "../database/db";
|
import db from "../database/db";
|
||||||
import {
|
import { FlowAuditLog, FlowIntegrationContext } from "../types/flow";
|
||||||
FlowAuditLog,
|
|
||||||
FlowIntegrationContext,
|
|
||||||
FlowDefinition,
|
|
||||||
} from "../types/flow";
|
|
||||||
import { FlowDefinitionService } from "./flowDefinitionService";
|
import { FlowDefinitionService } from "./flowDefinitionService";
|
||||||
import { FlowStepService } from "./flowStepService";
|
import { FlowStepService } from "./flowStepService";
|
||||||
import { FlowExternalDbIntegrationService } from "./flowExternalDbIntegrationService";
|
import { FlowExternalDbIntegrationService } from "./flowExternalDbIntegrationService";
|
||||||
import {
|
|
||||||
getExternalPool,
|
|
||||||
executeExternalQuery,
|
|
||||||
executeExternalTransaction,
|
|
||||||
} from "./externalDbHelper";
|
|
||||||
import {
|
|
||||||
getPlaceholder,
|
|
||||||
buildUpdateQuery,
|
|
||||||
buildInsertQuery,
|
|
||||||
buildSelectQuery,
|
|
||||||
} from "./dbQueryBuilder";
|
|
||||||
|
|
||||||
export class FlowDataMoveService {
|
export class FlowDataMoveService {
|
||||||
private flowDefinitionService: FlowDefinitionService;
|
private flowDefinitionService: FlowDefinitionService;
|
||||||
|
|
@ -48,28 +33,6 @@ export class FlowDataMoveService {
|
||||||
userId: string = "system",
|
userId: string = "system",
|
||||||
additionalData?: Record<string, any>
|
additionalData?: Record<string, any>
|
||||||
): Promise<{ success: boolean; targetDataId?: any; message?: string }> {
|
): 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) => {
|
return await db.transaction(async (client) => {
|
||||||
try {
|
try {
|
||||||
// 1. 단계 정보 조회
|
// 1. 단계 정보 조회
|
||||||
|
|
@ -197,14 +160,7 @@ export class FlowDataMoveService {
|
||||||
dataId: any,
|
dataId: any,
|
||||||
additionalData?: Record<string, any>
|
additionalData?: Record<string, any>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 상태 컬럼이 지정되지 않은 경우 에러
|
const statusColumn = toStep.statusColumn || "flow_status";
|
||||||
if (!toStep.statusColumn) {
|
|
||||||
throw new Error(
|
|
||||||
`단계 "${toStep.stepName}"의 상태 컬럼이 지정되지 않았습니다. 플로우 편집 화면에서 "상태 컬럼명"을 설정해주세요.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusColumn = toStep.statusColumn;
|
|
||||||
const tableName = fromStep.tableName;
|
const tableName = fromStep.tableName;
|
||||||
|
|
||||||
// 추가 필드 업데이트 준비
|
// 추가 필드 업데이트 준비
|
||||||
|
|
@ -634,307 +590,4 @@ export class FlowDataMoveService {
|
||||||
userId,
|
userId,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 DB 데이터 이동 처리
|
|
||||||
*/
|
|
||||||
private async moveDataToStepExternal(
|
|
||||||
dbConnectionId: number,
|
|
||||||
fromStepId: number,
|
|
||||||
toStepId: number,
|
|
||||||
dataId: any,
|
|
||||||
userId: string = "system",
|
|
||||||
additionalData?: Record<string, any>
|
|
||||||
): 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<string, any>
|
|
||||||
): Promise<void> {
|
|
||||||
// 상태 컬럼이 지정되지 않은 경우 에러
|
|
||||||
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<string, any>
|
|
||||||
): Promise<any> {
|
|
||||||
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<string, any> = {};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,33 +17,18 @@ export class FlowDefinitionService {
|
||||||
request: CreateFlowDefinitionRequest,
|
request: CreateFlowDefinitionRequest,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<FlowDefinition> {
|
): Promise<FlowDefinition> {
|
||||||
console.log("🔥 flowDefinitionService.create called with:", {
|
|
||||||
name: request.name,
|
|
||||||
description: request.description,
|
|
||||||
tableName: request.tableName,
|
|
||||||
dbSourceType: request.dbSourceType,
|
|
||||||
dbConnectionId: request.dbConnectionId,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO flow_definition (name, description, table_name, db_source_type, db_connection_id, created_by)
|
INSERT INTO flow_definition (name, description, table_name, created_by)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const values = [
|
const result = await db.query(query, [
|
||||||
request.name,
|
request.name,
|
||||||
request.description || null,
|
request.description || null,
|
||||||
request.tableName || null,
|
request.tableName,
|
||||||
request.dbSourceType || "internal",
|
|
||||||
request.dbConnectionId || null,
|
|
||||||
userId,
|
userId,
|
||||||
];
|
]);
|
||||||
|
|
||||||
console.log("💾 Executing INSERT with values:", values);
|
|
||||||
|
|
||||||
const result = await db.query(query, values);
|
|
||||||
|
|
||||||
return this.mapToFlowDefinition(result[0]);
|
return this.mapToFlowDefinition(result[0]);
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +162,6 @@ export class FlowDefinitionService {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
tableName: row.table_name,
|
tableName: row.table_name,
|
||||||
dbSourceType: row.db_source_type || "internal",
|
|
||||||
dbConnectionId: row.db_connection_id,
|
|
||||||
isActive: row.is_active,
|
isActive: row.is_active,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import { FlowStepDataCount, FlowStepDataList } from "../types/flow";
|
||||||
import { FlowDefinitionService } from "./flowDefinitionService";
|
import { FlowDefinitionService } from "./flowDefinitionService";
|
||||||
import { FlowStepService } from "./flowStepService";
|
import { FlowStepService } from "./flowStepService";
|
||||||
import { FlowConditionParser } from "./flowConditionParser";
|
import { FlowConditionParser } from "./flowConditionParser";
|
||||||
import { executeExternalQuery } from "./externalDbHelper";
|
|
||||||
import { getPlaceholder, buildPaginationClause } from "./dbQueryBuilder";
|
|
||||||
|
|
||||||
export class FlowExecutionService {
|
export class FlowExecutionService {
|
||||||
private flowDefinitionService: FlowDefinitionService;
|
private flowDefinitionService: FlowDefinitionService;
|
||||||
|
|
@ -30,13 +28,6 @@ export class FlowExecutionService {
|
||||||
throw new Error(`Flow definition not found: ${flowId}`);
|
throw new Error(`Flow definition not found: ${flowId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔍 [getStepDataCount] Flow Definition:", {
|
|
||||||
flowId,
|
|
||||||
dbSourceType: flowDef.dbSourceType,
|
|
||||||
dbConnectionId: flowDef.dbConnectionId,
|
|
||||||
tableName: flowDef.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 플로우 단계 조회
|
// 2. 플로우 단계 조회
|
||||||
const step = await this.flowStepService.findById(stepId);
|
const step = await this.flowStepService.findById(stepId);
|
||||||
if (!step) {
|
if (!step) {
|
||||||
|
|
@ -55,40 +46,11 @@ export class FlowExecutionService {
|
||||||
step.conditionJson
|
step.conditionJson
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. 카운트 쿼리 실행 (내부 또는 외부 DB)
|
// 5. 카운트 쿼리 실행
|
||||||
const query = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`;
|
const query = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`;
|
||||||
|
const result = await db.query(query, params);
|
||||||
|
|
||||||
console.log("🔍 [getStepDataCount] Query Info:", {
|
return parseInt(result[0].count);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -126,98 +88,47 @@ export class FlowExecutionService {
|
||||||
|
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const isExternalDb =
|
|
||||||
flowDef.dbSourceType === "external" && flowDef.dbConnectionId;
|
|
||||||
|
|
||||||
// 5. 전체 카운트
|
// 5. 전체 카운트
|
||||||
const countQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`;
|
const countQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where}`;
|
||||||
let countResult: any;
|
const countResult = await db.query(countQuery, params);
|
||||||
let total: number;
|
const total = parseInt(countResult[0].count);
|
||||||
|
|
||||||
if (isExternalDb) {
|
// 6. 테이블의 Primary Key 컬럼 찾기
|
||||||
const externalCountResult = await executeExternalQuery(
|
let orderByColumn = "";
|
||||||
flowDef.dbConnectionId!,
|
try {
|
||||||
countQuery,
|
const pkQuery = `
|
||||||
params
|
SELECT a.attname
|
||||||
);
|
FROM pg_index i
|
||||||
countResult = externalCountResult.rows;
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
||||||
total = parseInt(countResult[0].count || countResult[0].COUNT);
|
WHERE i.indrelid = $1::regclass
|
||||||
} else {
|
AND i.indisprimary
|
||||||
countResult = await db.query(countQuery, params);
|
LIMIT 1
|
||||||
total = parseInt(countResult[0].count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 데이터 조회 (DB 타입별 페이징 처리)
|
|
||||||
let dataQuery: string;
|
|
||||||
let dataParams: any[];
|
|
||||||
|
|
||||||
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 pkResult = await db.query(pkQuery, [tableName]);
|
||||||
|
if (pkResult.length > 0) {
|
||||||
const externalDataResult = await executeExternalQuery(
|
orderByColumn = pkResult[0].attname;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
const orderByClause = orderByColumn
|
// Primary Key를 찾지 못하면 ORDER BY 없이 진행
|
||||||
? `ORDER BY ${orderByColumn} DESC`
|
console.warn(`Could not find primary key for table ${tableName}:`, err);
|
||||||
: "";
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: dataResult,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ export interface FlowDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external"; // 데이터베이스 소스 타입
|
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
@ -21,8 +19,6 @@ export interface CreateFlowDefinitionRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external"; // 데이터베이스 소스 타입
|
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 플로우 정의 수정 요청
|
// 플로우 정의 수정 요청
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,7 @@ export default function FlowEditorPage() {
|
||||||
// 플로우 정의 로드
|
// 플로우 정의 로드
|
||||||
const flowRes = await getFlowDefinition(flowId);
|
const flowRes = await getFlowDefinition(flowId);
|
||||||
if (flowRes.success && flowRes.data) {
|
if (flowRes.success && flowRes.data) {
|
||||||
console.log("🔍 Flow Definition loaded:", flowRes.data);
|
setFlowDefinition(flowRes.data);
|
||||||
console.log("📋 Table Name:", flowRes.data.definition?.tableName);
|
|
||||||
setFlowDefinition(flowRes.data.definition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 단계 로드
|
// 단계 로드
|
||||||
|
|
@ -316,9 +314,6 @@ export default function FlowEditorPage() {
|
||||||
<FlowStepPanel
|
<FlowStepPanel
|
||||||
step={selectedStep}
|
step={selectedStep}
|
||||||
flowId={flowId}
|
flowId={flowId}
|
||||||
flowTableName={flowDefinition?.tableName} // 플로우 정의의 테이블명 전달
|
|
||||||
flowDbSourceType={flowDefinition?.dbSourceType} // DB 소스 타입 전달
|
|
||||||
flowDbConnectionId={flowDefinition?.dbConnectionId} // 외부 DB 연결 ID 전달
|
|
||||||
onClose={() => setSelectedStep(null)}
|
onClose={() => setSelectedStep(null)}
|
||||||
onUpdate={loadFlowData}
|
onUpdate={loadFlowData}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User, Check, ChevronsUpDown } from "lucide-react";
|
import { Plus, Edit2, Trash2, Play, Workflow, Table, Calendar, User } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -27,11 +27,6 @@ import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { getFlowDefinitions, createFlowDefinition, deleteFlowDefinition } from "@/lib/api/flow";
|
import { getFlowDefinitions, createFlowDefinition, deleteFlowDefinition } from "@/lib/api/flow";
|
||||||
import { FlowDefinition } from "@/types/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() {
|
export default function FlowManagementPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -44,15 +39,6 @@ export default function FlowManagementPage() {
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [selectedFlow, setSelectedFlow] = useState<FlowDefinition | null>(null);
|
const [selectedFlow, setSelectedFlow] = useState<FlowDefinition | null>(null);
|
||||||
|
|
||||||
// 테이블 목록 관련 상태
|
|
||||||
const [tableList, setTableList] = useState<any[]>([]); // 내부 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<any[]>([]);
|
|
||||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
|
||||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
|
||||||
|
|
||||||
// 생성 폼 상태
|
// 생성 폼 상태
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -89,107 +75,9 @@ export default function FlowManagementPage() {
|
||||||
loadFlows();
|
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 () => {
|
const handleCreate = async () => {
|
||||||
console.log("🚀 handleCreate called with formData:", formData);
|
|
||||||
|
|
||||||
if (!formData.name || !formData.tableName) {
|
if (!formData.name || !formData.tableName) {
|
||||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName });
|
|
||||||
toast({
|
toast({
|
||||||
title: "입력 오류",
|
title: "입력 오류",
|
||||||
description: "플로우 이름과 테이블 이름은 필수입니다.",
|
description: "플로우 이름과 테이블 이름은 필수입니다.",
|
||||||
|
|
@ -199,15 +87,7 @@ export default function FlowManagementPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// DB 소스 정보 추가
|
const response = await createFlowDefinition(formData);
|
||||||
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) {
|
if (response.success && response.data) {
|
||||||
toast({
|
toast({
|
||||||
title: "생성 완료",
|
title: "생성 완료",
|
||||||
|
|
@ -215,7 +95,6 @@ export default function FlowManagementPage() {
|
||||||
});
|
});
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
setFormData({ name: "", description: "", tableName: "" });
|
setFormData({ name: "", description: "", tableName: "" });
|
||||||
setSelectedDbSource("internal");
|
|
||||||
loadFlows();
|
loadFlows();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -398,123 +277,19 @@ export default function FlowManagementPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DB 소스 선택 */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs sm:text-sm">데이터베이스 소스</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedDbSource.toString()}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const dbSource = value === "internal" ? "internal" : parseInt(value);
|
|
||||||
setSelectedDbSource(dbSource);
|
|
||||||
// DB 소스 변경 시 테이블 선택 초기화
|
|
||||||
setFormData({ ...formData, tableName: "" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
||||||
<SelectValue placeholder="데이터베이스 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="internal">내부 데이터베이스</SelectItem>
|
|
||||||
{externalConnections.map((conn: any) => (
|
|
||||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
|
||||||
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
|
||||||
플로우에서 사용할 데이터베이스를 선택합니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테이블 선택 */}
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
||||||
연결 테이블 *
|
연결 테이블 *
|
||||||
</Label>
|
</Label>
|
||||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
<Input
|
||||||
<PopoverTrigger asChild>
|
id="tableName"
|
||||||
<Button
|
value={formData.tableName}
|
||||||
variant="outline"
|
onChange={(e) => setFormData({ ...formData, tableName: e.target.value })}
|
||||||
role="combobox"
|
placeholder="예: products"
|
||||||
aria-expanded={openTableCombobox}
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
/>
|
||||||
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
|
||||||
>
|
|
||||||
{formData.tableName
|
|
||||||
? selectedDbSource === "internal"
|
|
||||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
|
||||||
formData.tableName
|
|
||||||
: formData.tableName
|
|
||||||
: loadingTables || loadingExternalTables
|
|
||||||
? "로딩 중..."
|
|
||||||
: "테이블 선택"}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{selectedDbSource === "internal"
|
|
||||||
? // 내부 DB 테이블 목록
|
|
||||||
tableList.map((table) => (
|
|
||||||
<CommandItem
|
|
||||||
key={table.tableName}
|
|
||||||
value={table.tableName}
|
|
||||||
onSelect={(currentValue) => {
|
|
||||||
console.log("📝 Internal table selected:", {
|
|
||||||
tableName: table.tableName,
|
|
||||||
currentValue,
|
|
||||||
});
|
|
||||||
setFormData({ ...formData, tableName: currentValue });
|
|
||||||
setOpenTableCombobox(false);
|
|
||||||
}}
|
|
||||||
className="text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
|
||||||
{table.description && (
|
|
||||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))
|
|
||||||
: // 외부 DB 테이블 목록
|
|
||||||
externalTableList.map((tableName, index) => (
|
|
||||||
<CommandItem
|
|
||||||
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
|
||||||
value={tableName}
|
|
||||||
onSelect={(currentValue) => {
|
|
||||||
setFormData({ ...formData, tableName: currentValue });
|
|
||||||
setOpenTableCombobox(false);
|
|
||||||
}}
|
|
||||||
className="text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>{tableName}</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
|
플로우가 관리할 데이터 테이블 이름을 입력하세요
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge";
|
||||||
*/
|
*/
|
||||||
export default function MainPage() {
|
export default function MainPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-4 pt-10">
|
<div className="space-y-6 pt-10">
|
||||||
{/* 메인 컨텐츠 */}
|
{/* 메인 컨텐츠 */}
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -18,7 +18,7 @@ export default function MainPage() {
|
||||||
<h3 className="text-lg font-semibold">Vexolor에 오신 것을 환영합니다!</h3>
|
<h3 className="text-lg font-semibold">Vexolor에 오신 것을 환영합니다!</h3>
|
||||||
<p className="text-muted-foreground">제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.</p>
|
<p className="text-muted-foreground">제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.</p>
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
<Badge variant="secondary">Node.js</Badge>
|
<Badge variant="secondary">Spring Boot</Badge>
|
||||||
<Badge variant="secondary">Next.js</Badge>
|
<Badge variant="secondary">Next.js</Badge>
|
||||||
<Badge variant="secondary">Shadcn/ui</Badge>
|
<Badge variant="secondary">Shadcn/ui</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
export default function MainHomePage() {
|
export default function MainHomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 px-4 pt-10">
|
<div className="pt-10 space-y-6">
|
||||||
{/* 대시보드 컨텐츠 */}
|
{/* 대시보드 컨텐츠 */}
|
||||||
<div className="rounded-lg border bg-white p-6 shadow-sm">
|
<div className="rounded-lg border bg-white p-6 shadow-sm">
|
||||||
<h3 className="mb-4 text-lg font-semibold">WACE 솔루션에 오신 것을 환영합니다!</h3>
|
<h3 className="mb-4 text-lg font-semibold">WACE 솔루션에 오신 것을 환영합니다!</h3>
|
||||||
<p className="mb-6 text-gray-600">제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.</p>
|
<p className="mb-6 text-gray-600">제품 수명 주기 관리 시스템을 통해 효율적인 업무를 시작하세요.</p>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-blue-700/10 ring-inset">
|
||||||
|
Spring Boot
|
||||||
|
</span>
|
||||||
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-700/10 ring-inset">
|
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-700/10 ring-inset">
|
||||||
Next.js
|
Next.js
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ import { getTableColumns } from "@/lib/api/tableManagement";
|
||||||
interface FlowConditionBuilderProps {
|
interface FlowConditionBuilderProps {
|
||||||
flowId: number;
|
flowId: number;
|
||||||
tableName?: string; // 조회할 테이블명
|
tableName?: string; // 조회할 테이블명
|
||||||
dbSourceType?: "internal" | "external"; // DB 소스 타입
|
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
|
||||||
condition?: FlowConditionGroup;
|
condition?: FlowConditionGroup;
|
||||||
onChange: (condition: FlowConditionGroup | undefined) => void;
|
onChange: (condition: FlowConditionGroup | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -37,14 +35,7 @@ const OPERATORS: { value: ConditionOperator; label: string }[] = [
|
||||||
{ value: "is_not_null", label: "NOT NULL" },
|
{ value: "is_not_null", label: "NOT NULL" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function FlowConditionBuilder({
|
export function FlowConditionBuilder({ flowId, tableName, condition, onChange }: FlowConditionBuilderProps) {
|
||||||
flowId,
|
|
||||||
tableName,
|
|
||||||
dbSourceType = "internal",
|
|
||||||
dbConnectionId,
|
|
||||||
condition,
|
|
||||||
onChange,
|
|
||||||
}: FlowConditionBuilderProps) {
|
|
||||||
const [columns, setColumns] = useState<any[]>([]);
|
const [columns, setColumns] = useState<any[]>([]);
|
||||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||||
const [conditionType, setConditionType] = useState<"AND" | "OR">(condition?.type || "AND");
|
const [conditionType, setConditionType] = useState<"AND" | "OR">(condition?.type || "AND");
|
||||||
|
|
@ -61,7 +52,7 @@ export function FlowConditionBuilder({
|
||||||
}
|
}
|
||||||
}, [condition]);
|
}, [condition]);
|
||||||
|
|
||||||
// 테이블 컬럼 로드 - 내부/외부 DB 모두 지원
|
// 테이블 컬럼 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
|
|
@ -71,69 +62,17 @@ export function FlowConditionBuilder({
|
||||||
const loadColumns = async () => {
|
const loadColumns = async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingColumns(true);
|
setLoadingColumns(true);
|
||||||
console.log("🔍 [FlowConditionBuilder] Loading columns:", {
|
console.log("🔍 Loading columns for table:", tableName);
|
||||||
tableName,
|
const response = await getTableColumns(tableName);
|
||||||
dbSourceType,
|
console.log("📦 Column API response:", response);
|
||||||
dbConnectionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 외부 DB인 경우
|
if (response.success && response.data?.columns) {
|
||||||
if (dbSourceType === "external" && dbConnectionId) {
|
const columnArray = Array.isArray(response.data.columns) ? response.data.columns : [];
|
||||||
const token = localStorage.getItem("authToken");
|
console.log("✅ Setting columns:", columnArray.length, "items");
|
||||||
if (!token) {
|
setColumns(columnArray);
|
||||||
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 {
|
} else {
|
||||||
// 내부 DB인 경우 (기존 로직)
|
console.error("❌ Failed to load columns:", response.message);
|
||||||
const response = await getTableColumns(tableName);
|
setColumns([]);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("❌ Exception loading columns:", error);
|
console.error("❌ Exception loading columns:", error);
|
||||||
|
|
@ -144,7 +83,7 @@ export function FlowConditionBuilder({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [tableName, dbSourceType, dbConnectionId]);
|
}, [tableName]);
|
||||||
|
|
||||||
// 조건 변경 시 부모에 전달
|
// 조건 변경 시 부모에 전달
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -31,35 +31,16 @@ import { Textarea } from "@/components/ui/textarea";
|
||||||
interface FlowStepPanelProps {
|
interface FlowStepPanelProps {
|
||||||
step: FlowStep;
|
step: FlowStep;
|
||||||
flowId: number;
|
flowId: number;
|
||||||
flowTableName?: string; // 플로우 정의에서 선택한 테이블명
|
|
||||||
flowDbSourceType?: "internal" | "external"; // 플로우의 DB 소스 타입
|
|
||||||
flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlowStepPanel({
|
export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanelProps) {
|
||||||
step,
|
|
||||||
flowId,
|
|
||||||
flowTableName,
|
|
||||||
flowDbSourceType = "internal",
|
|
||||||
flowDbConnectionId,
|
|
||||||
onClose,
|
|
||||||
onUpdate,
|
|
||||||
}: FlowStepPanelProps) {
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
console.log("🎯 FlowStepPanel Props:", {
|
|
||||||
stepTableName: step.tableName,
|
|
||||||
flowTableName,
|
|
||||||
flowDbSourceType,
|
|
||||||
flowDbConnectionId,
|
|
||||||
final: step.tableName || flowTableName || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
stepName: step.stepName,
|
stepName: step.stepName,
|
||||||
tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용 (신규 방식)
|
tableName: step.tableName || "",
|
||||||
conditionJson: step.conditionJson,
|
conditionJson: step.conditionJson,
|
||||||
// 하이브리드 모드 필드
|
// 하이브리드 모드 필드
|
||||||
moveType: step.moveType || "status",
|
moveType: step.moveType || "status",
|
||||||
|
|
@ -234,12 +215,11 @@ export function FlowStepPanel({
|
||||||
stepName: step.stepName,
|
stepName: step.stepName,
|
||||||
statusColumn: step.statusColumn,
|
statusColumn: step.statusColumn,
|
||||||
statusValue: step.statusValue,
|
statusValue: step.statusValue,
|
||||||
flowTableName, // 플로우 정의의 테이블명
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const newFormData = {
|
const newFormData = {
|
||||||
stepName: step.stepName,
|
stepName: step.stepName,
|
||||||
tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용
|
tableName: step.tableName || "",
|
||||||
conditionJson: step.conditionJson,
|
conditionJson: step.conditionJson,
|
||||||
// 하이브리드 모드 필드
|
// 하이브리드 모드 필드
|
||||||
moveType: step.moveType || "status",
|
moveType: step.moveType || "status",
|
||||||
|
|
@ -254,9 +234,9 @@ export function FlowStepPanel({
|
||||||
|
|
||||||
console.log("✅ Setting formData:", newFormData);
|
console.log("✅ Setting formData:", newFormData);
|
||||||
setFormData(newFormData);
|
setFormData(newFormData);
|
||||||
}, [step.id, flowTableName]); // flowTableName도 의존성 추가
|
}, [step.id]); // step 전체가 아닌 step.id만 의존성으로 설정
|
||||||
|
|
||||||
// 테이블 선택 시 컬럼 로드 - 내부/외부 DB 모두 지원
|
// 테이블 선택 시 컬럼 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadColumns = async () => {
|
const loadColumns = async () => {
|
||||||
if (!formData.tableName) {
|
if (!formData.tableName) {
|
||||||
|
|
@ -266,70 +246,16 @@ export function FlowStepPanel({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoadingColumns(true);
|
setLoadingColumns(true);
|
||||||
console.log("🔍 Loading columns for status column selector:", {
|
console.log("🔍 Loading columns for status column selector:", formData.tableName);
|
||||||
tableName: formData.tableName,
|
const response = await getTableColumns(formData.tableName);
|
||||||
flowDbSourceType,
|
console.log("📦 Columns response:", response);
|
||||||
flowDbConnectionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 외부 DB인 경우
|
if (response.success && response.data && response.data.columns) {
|
||||||
if (flowDbSourceType === "external" && flowDbConnectionId) {
|
console.log("✅ Setting columns:", response.data.columns);
|
||||||
const token = localStorage.getItem("authToken");
|
setColumns(response.data.columns);
|
||||||
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 {
|
} else {
|
||||||
// 내부 DB인 경우 (기존 로직)
|
console.log("❌ No columns in response");
|
||||||
const response = await getTableColumns(formData.tableName);
|
setColumns([]);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Failed to load columns:", error);
|
console.error("Failed to load columns:", error);
|
||||||
|
|
@ -340,7 +266,7 @@ export function FlowStepPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [formData.tableName, flowDbSourceType, flowDbConnectionId]);
|
}, [formData.tableName]);
|
||||||
|
|
||||||
// formData의 최신 값을 항상 참조하기 위한 ref
|
// formData의 최신 값을 항상 참조하기 위한 ref
|
||||||
const formDataRef = useRef(formData);
|
const formDataRef = useRef(formData);
|
||||||
|
|
@ -354,27 +280,6 @@ export function FlowStepPanel({
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
const currentFormData = formDataRef.current;
|
const currentFormData = formDataRef.current;
|
||||||
console.log("🚀 handleSave called, formData:", JSON.stringify(currentFormData, null, 2));
|
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 {
|
try {
|
||||||
const response = await updateFlowStep(step.id, currentFormData);
|
const response = await updateFlowStep(step.id, currentFormData);
|
||||||
console.log("📡 API response:", response);
|
console.log("📡 API response:", response);
|
||||||
|
|
@ -463,9 +368,8 @@ export function FlowStepPanel({
|
||||||
<Input value={step.stepOrder} disabled />
|
<Input value={step.stepOrder} disabled />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== 구버전: 단계별 테이블 선택 방식 (주석처리) ===== */}
|
|
||||||
{/* DB 소스 선택 */}
|
{/* DB 소스 선택 */}
|
||||||
{/* <div>
|
<div>
|
||||||
<Label>데이터베이스 소스</Label>
|
<Label>데이터베이스 소스</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedDbSource.toString()}
|
value={selectedDbSource.toString()}
|
||||||
|
|
@ -489,10 +393,10 @@ export function FlowStepPanel({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="mt-1 text-xs text-gray-500">조회할 데이터베이스를 선택합니다</p>
|
<p className="mt-1 text-xs text-gray-500">조회할 데이터베이스를 선택합니다</p>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{/* 테이블 선택 */}
|
{/* 테이블 선택 */}
|
||||||
{/* <div>
|
<div>
|
||||||
<Label>조회할 테이블</Label>
|
<Label>조회할 테이블</Label>
|
||||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
@ -574,16 +478,7 @@ export function FlowStepPanel({
|
||||||
? "이 단계에서 조건을 적용할 테이블을 선택합니다"
|
? "이 단계에서 조건을 적용할 테이블을 선택합니다"
|
||||||
: "외부 데이터베이스의 테이블을 선택합니다"}
|
: "외부 데이터베이스의 테이블을 선택합니다"}
|
||||||
</p>
|
</p>
|
||||||
</div> */}
|
|
||||||
{/* ===== 구버전 끝 ===== */}
|
|
||||||
|
|
||||||
{/* ===== 신버전: 플로우에서 선택한 테이블 표시만 ===== */}
|
|
||||||
<div>
|
|
||||||
<Label>연결된 테이블</Label>
|
|
||||||
<Input value={formData.tableName || "테이블이 지정되지 않았습니다"} disabled className="bg-gray-50" />
|
|
||||||
<p className="mt-1 text-xs text-gray-500">플로우 생성 시 선택한 테이블입니다 (수정 불가)</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/* ===== 신버전 끝 ===== */}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -600,8 +495,6 @@ export function FlowStepPanel({
|
||||||
<FlowConditionBuilder
|
<FlowConditionBuilder
|
||||||
flowId={flowId}
|
flowId={flowId}
|
||||||
tableName={formData.tableName}
|
tableName={formData.tableName}
|
||||||
dbSourceType={flowDbSourceType}
|
|
||||||
dbConnectionId={flowDbConnectionId}
|
|
||||||
condition={formData.conditionJson}
|
condition={formData.conditionJson}
|
||||||
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue