ERP-node/backend-node/src/services/multiConnectionQueryService.ts

1148 lines
37 KiB
TypeScript

/**
* 다중 커넥션 쿼리 실행 서비스
* 외부 데이터베이스 커넥션을 통한 CRUD 작업 지원
* 자기 자신 테이블 작업을 위한 안전장치 포함
*/
import { ExternalDbConnectionService } from "./externalDbConnectionService";
import { TableManagementService } from "./tableManagementService";
import { ExternalDbConnection, ApiResponse } from "../types/externalDbTypes";
import { ColumnTypeInfo, TableInfo } from "../types/tableManagement";
import prisma from "../config/database";
import { logger } from "../utils/logger";
// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용
export interface ValidationResult {
isValid: boolean;
error?: string;
warnings?: string[];
}
export interface ColumnInfo {
columnName: string;
displayName: string;
dataType: string;
dbType: string;
webType: string;
isNullable: boolean;
isPrimaryKey: boolean;
defaultValue?: string;
maxLength?: number;
description?: string;
}
export interface MultiConnectionTableInfo {
tableName: string;
displayName?: string;
columnCount: number;
connectionId: number;
connectionName: string;
dbType: string;
}
export class MultiConnectionQueryService {
private tableManagementService: TableManagementService;
constructor() {
this.tableManagementService = new TableManagementService();
}
/**
* 소스 커넥션에서 데이터 조회
*/
async fetchDataFromConnection(
connectionId: number,
tableName: string,
conditions?: Record<string, any>
): Promise<Record<string, any>[]> {
try {
logger.info(
`데이터 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"select",
tableName,
undefined,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 쿼리 조건 구성
let whereClause = "";
const queryParams: any[] = [];
if (conditions && Object.keys(conditions).length > 0) {
const conditionParts: string[] = [];
let paramIndex = 1;
Object.entries(conditions).forEach(([key, value]) => {
conditionParts.push(`${key} = $${paramIndex}`);
queryParams.push(value);
paramIndex++;
});
whereClause = `WHERE ${conditionParts.join(" AND ")}`;
}
const query = `SELECT * FROM ${tableName} ${whereClause}`;
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query
);
if (!result.success || !result.data) {
throw new Error(result.message || "쿼리 실행 실패");
}
logger.info(`데이터 조회 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 조회 실패: ${error}`);
throw new Error(
`데이터 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 대상 커넥션에 데이터 삽입
*/
async insertDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
logger.info(
`데이터 삽입 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase("insert", tableName, data);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// INSERT 쿼리 구성 (DB 타입별 처리)
const columns = Object.keys(data);
let values = Object.values(data);
// Oracle의 경우 테이블 스키마 확인 및 데이터 타입 변환 처리
if (connection.db_type?.toLowerCase() === "oracle") {
try {
// Oracle 테이블 스키마 조회
const schemaQuery = `
SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_DEFAULT
FROM USER_TAB_COLUMNS
WHERE TABLE_NAME = UPPER('${tableName}')
ORDER BY COLUMN_ID
`;
logger.info(`🔍 Oracle 테이블 스키마 조회: ${schemaQuery}`);
const schemaResult = await ExternalDbConnectionService.executeQuery(
connectionId,
schemaQuery
);
if (schemaResult.success && schemaResult.data) {
logger.info(`📋 Oracle 테이블 ${tableName} 스키마:`);
schemaResult.data.forEach((col: any) => {
logger.info(
` - ${col.COLUMN_NAME}: ${col.DATA_TYPE}, NULL: ${col.NULLABLE}, DEFAULT: ${col.DATA_DEFAULT || "None"}`
);
});
// 필수 컬럼 중 누락된 컬럼이 있는지 확인 (기본값이 없는 NOT NULL 컬럼만)
const providedColumns = columns.map((col) => col.toUpperCase());
const missingRequiredColumns = schemaResult.data.filter(
(schemaCol: any) =>
schemaCol.NULLABLE === "N" &&
!schemaCol.DATA_DEFAULT &&
!providedColumns.includes(schemaCol.COLUMN_NAME)
);
if (missingRequiredColumns.length > 0) {
const missingNames = missingRequiredColumns.map(
(col: any) => col.COLUMN_NAME
);
logger.error(`❌ 필수 컬럼 누락: ${missingNames.join(", ")}`);
throw new Error(
`필수 컬럼이 누락되었습니다: ${missingNames.join(", ")}`
);
}
logger.info(
`✅ 스키마 검증 통과: 모든 필수 컬럼이 제공되었거나 기본값이 있습니다.`
);
}
} catch (schemaError) {
logger.warn(`⚠️ 스키마 조회 실패 (계속 진행): ${schemaError}`);
}
values = values.map((value) => {
// null이나 undefined는 그대로 유지
if (value === null || value === undefined) {
return value;
}
// 숫자로 변환 가능한 문자열은 숫자로 변환
if (typeof value === "string" && value.trim() !== "") {
const numValue = Number(value);
if (!isNaN(numValue)) {
logger.info(
`🔄 Oracle 데이터 타입 변환: "${value}" (string) → ${numValue} (number)`
);
return numValue;
}
}
return value;
});
}
let query: string;
let queryParams: any[];
const dbType = connection.db_type?.toLowerCase() || "postgresql";
switch (dbType) {
case "oracle":
// Oracle: :1, :2 스타일 바인딩 사용, RETURNING 미지원
const oraclePlaceholders = values
.map((_, index) => `:${index + 1}`)
.join(", ");
query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${oraclePlaceholders})`;
queryParams = values;
logger.info(`🔍 Oracle INSERT 상세 정보:`);
logger.info(` - 테이블: ${tableName}`);
logger.info(` - 컬럼: ${JSON.stringify(columns)}`);
logger.info(` - 값: ${JSON.stringify(values)}`);
logger.info(` - 쿼리: ${query}`);
logger.info(` - 파라미터: ${JSON.stringify(queryParams)}`);
logger.info(
` - 데이터 타입: ${JSON.stringify(values.map((v) => typeof v))}`
);
break;
case "mysql":
case "mariadb":
// MySQL/MariaDB: ? 스타일 바인딩 사용, RETURNING 미지원
const mysqlPlaceholders = values.map(() => "?").join(", ");
query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${mysqlPlaceholders})`;
queryParams = values;
logger.info(`MySQL/MariaDB INSERT 쿼리:`, {
query,
params: queryParams,
});
break;
case "sqlserver":
case "mssql":
// SQL Server: @param1, @param2 스타일 바인딩 사용
const sqlServerPlaceholders = values
.map((_, index) => `@param${index + 1}`)
.join(", ");
query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${sqlServerPlaceholders})`;
queryParams = values;
logger.info(`SQL Server INSERT 쿼리:`, {
query,
params: queryParams,
});
break;
case "sqlite":
// SQLite: ? 스타일 바인딩 사용, RETURNING 지원 (3.35.0+)
const sqlitePlaceholders = values.map(() => "?").join(", ");
query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${sqlitePlaceholders}) RETURNING *`;
queryParams = values;
logger.info(`SQLite INSERT 쿼리:`, { query, params: queryParams });
break;
case "postgresql":
default:
// PostgreSQL: $1, $2 스타일 바인딩 사용, RETURNING 지원
const pgPlaceholders = values
.map((_, index) => `$${index + 1}`)
.join(", ");
query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${pgPlaceholders}) RETURNING *`;
queryParams = values;
logger.info(`PostgreSQL INSERT 쿼리:`, {
query,
params: queryParams,
});
break;
}
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query,
queryParams
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 삽입 실패");
}
logger.info(`데이터 삽입 완료`);
return result.data[0] || result.data;
} catch (error) {
logger.error(`데이터 삽입 실패: ${error}`);
throw new Error(
`데이터 삽입 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕 대상 커넥션에 데이터 업데이트
*/
async updateDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>,
conditions: Record<string, any>
): Promise<any> {
try {
logger.info(
`데이터 업데이트 시작: connectionId=${connectionId}, table=${tableName}`
);
// 자기 자신 테이블 작업 검증
if (connectionId === 0) {
const validationResult = await this.validateSelfTableOperation(
tableName,
"update",
[conditions]
);
if (!validationResult.isValid) {
throw new Error(
`자기 자신 테이블 업데이트 검증 실패: ${validationResult.error}`
);
}
}
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"update",
tableName,
data,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// UPDATE 쿼리 구성
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const whereClause = Object.keys(conditions)
.map(
(key, index) => `${key} = $${Object.keys(data).length + index + 1}`
)
.join(" AND ");
const query = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${whereClause}
RETURNING *
`;
const queryParams = [
...Object.values(data),
...Object.values(conditions),
];
// 외부 DB에서 쿼리 실행
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
query
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 업데이트 실패");
}
logger.info(`데이터 업데이트 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 업데이트 실패: ${error}`);
throw new Error(
`데이터 업데이트 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕 대상 커넥션에서 데이터 삭제
*/
async deleteDataFromConnection(
connectionId: number,
tableName: string,
conditions: Record<string, any>,
maxDeleteCount: number = 100
): Promise<any> {
try {
logger.info(
`데이터 삭제 시작: connectionId=${connectionId}, table=${tableName}`
);
// 자기 자신 테이블 작업 검증
if (connectionId === 0) {
const validationResult = await this.validateSelfTableOperation(
tableName,
"delete",
[conditions]
);
if (!validationResult.isValid) {
throw new Error(
`자기 자신 테이블 삭제 검증 실패: ${validationResult.error}`
);
}
}
// WHERE 조건 필수 체크
if (!conditions || Object.keys(conditions).length === 0) {
throw new Error("DELETE 작업에는 반드시 WHERE 조건이 필요합니다.");
}
// connectionId가 0이면 메인 DB 사용
if (connectionId === 0) {
return await this.executeOnMainDatabase(
"delete",
tableName,
undefined,
conditions
);
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 먼저 삭제 대상 개수 확인 (안전장치)
const countQuery = `
SELECT COUNT(*) as count
FROM ${tableName}
WHERE ${Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ")}
`;
const countResult = await ExternalDbConnectionService.executeQuery(
connectionId,
countQuery
);
if (!countResult.success || !countResult.data) {
throw new Error(countResult.message || "삭제 대상 개수 조회 실패");
}
const deleteCount = parseInt(countResult.data[0]?.count || "0");
if (deleteCount > maxDeleteCount) {
throw new Error(
`삭제 대상이 ${deleteCount}건으로 최대 허용 개수(${maxDeleteCount})를 초과합니다.`
);
}
// DELETE 쿼리 실행
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ")}
RETURNING *
`;
const result = await ExternalDbConnectionService.executeQuery(
connectionId,
deleteQuery
);
if (!result.success || !result.data) {
throw new Error(result.message || "데이터 삭제 실패");
}
logger.info(`데이터 삭제 완료: ${result.data.length}`);
return result.data;
} catch (error) {
logger.error(`데이터 삭제 실패: ${error}`);
throw new Error(
`데이터 삭제 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 커넥션별 테이블 목록 조회
*/
async getTablesFromConnection(
connectionId: number
): Promise<MultiConnectionTableInfo[]> {
try {
logger.info(`테이블 목록 조회 시작: connectionId=${connectionId}`);
// connectionId가 0이면 메인 DB의 테이블 목록 반환
if (connectionId === 0) {
const tables = await this.tableManagementService.getTableList();
return tables.map((table) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName, // 라벨이 있으면 라벨 사용, 없으면 테이블명
columnCount: table.columnCount,
connectionId: 0,
connectionName: "메인 데이터베이스",
dbType: "postgresql",
}));
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 외부 DB의 테이블 목록 조회
const tablesResult =
await ExternalDbConnectionService.getTables(connectionId);
if (!tablesResult.success || !tablesResult.data) {
throw new Error(tablesResult.message || "테이블 조회 실패");
}
const tables = tablesResult.data;
// 성능 최적화: 컬럼 개수는 실제 필요할 때만 조회하도록 변경
return tables.map((table: any) => ({
tableName: table.table_name,
displayName: table.table_comment || table.table_name, // 라벨(comment)이 있으면 라벨 사용, 없으면 테이블명
columnCount: 0, // 성능을 위해 0으로 설정, 필요시 별도 API로 조회
connectionId: connectionId,
connectionName: connection.connection_name,
dbType: connection.db_type,
}));
} catch (error) {
logger.error(`테이블 목록 조회 실패: ${error}`);
throw new Error(
`테이블 목록 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 배치 테이블 정보 조회 (컬럼 수 포함)
*/
async getBatchTablesWithColumns(
connectionId: number
): Promise<
{ tableName: string; displayName?: string; columnCount: number }[]
> {
try {
logger.info(`배치 테이블 정보 조회 시작: connectionId=${connectionId}`);
// connectionId가 0이면 메인 DB
if (connectionId === 0) {
console.log("🔍 메인 DB 배치 테이블 정보 조회");
// 메인 DB의 모든 테이블과 각 테이블의 컬럼 수 조회
const tables = await this.tableManagementService.getTableList();
const result = await Promise.all(
tables.map(async (table) => {
try {
const columnsResult =
await this.tableManagementService.getColumnList(
table.tableName,
1,
1000
);
return {
tableName: table.tableName,
displayName: table.displayName,
columnCount: columnsResult.columns.length,
};
} catch (error) {
logger.warn(
`메인 DB 테이블 ${table.tableName} 컬럼 수 조회 실패:`,
error
);
return {
tableName: table.tableName,
displayName: table.displayName,
columnCount: 0,
};
}
})
);
logger.info(`✅ 메인 DB 배치 조회 완료: ${result.length}개 테이블`);
return result;
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
console.log(
`🔍 외부 DB 배치 테이블 정보 조회: connectionId=${connectionId}`
);
// 외부 DB의 테이블 목록 먼저 조회
const tablesResult =
await ExternalDbConnectionService.getTables(connectionId);
if (!tablesResult.success || !tablesResult.data) {
throw new Error("외부 DB 테이블 목록 조회 실패");
}
const tableNames = tablesResult.data;
// 🔧 각 테이블의 컬럼 수를 순차적으로 조회 (타임아웃 방지)
const result = [];
logger.info(
`📊 외부 DB 테이블 컬럼 조회 시작: ${tableNames.length}개 테이블`
);
for (let i = 0; i < tableNames.length; i++) {
const tableInfo = tableNames[i];
const tableName = tableInfo.table_name;
try {
logger.info(
`📋 테이블 ${i + 1}/${tableNames.length}: ${tableName} 컬럼 조회 중...`
);
// 🔧 타임아웃과 재시도 로직 추가
let columnsResult: ApiResponse<any[]> | undefined;
let retryCount = 0;
const maxRetries = 2;
while (retryCount <= maxRetries) {
try {
columnsResult = (await Promise.race([
ExternalDbConnectionService.getTableColumns(
connectionId,
tableName
),
new Promise<ApiResponse<any[]>>((_, reject) =>
setTimeout(
() => reject(new Error("컬럼 조회 타임아웃 (15초)")),
15000
)
),
])) as ApiResponse<any[]>;
break; // 성공하면 루프 종료
} catch (attemptError) {
retryCount++;
if (retryCount > maxRetries) {
throw attemptError; // 최대 재시도 후 에러 throw
}
logger.warn(
`⚠️ 테이블 ${tableName} 컬럼 조회 실패 (${retryCount}/${maxRetries}), 재시도 중...`
);
await new Promise((resolve) => setTimeout(resolve, 1000)); // 1초 대기 후 재시도
}
}
const columnCount =
columnsResult &&
columnsResult.success &&
Array.isArray(columnsResult.data)
? columnsResult.data.length
: 0;
result.push({
tableName,
displayName: tableName, // 외부 DB는 일반적으로 displayName이 없음
columnCount,
});
logger.info(`✅ 테이블 ${tableName}: ${columnCount}개 컬럼`);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
logger.warn(
`❌ 외부 DB 테이블 ${tableName} 컬럼 수 조회 최종 실패: ${errorMessage}`
);
result.push({
tableName,
displayName: tableName,
columnCount: 0, // 실패한 경우 0으로 설정
});
}
// 🔧 연결 부하 방지를 위한 약간의 지연
if (i < tableNames.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms 지연
}
}
logger.info(`✅ 외부 DB 배치 조회 완료: ${result.length}개 테이블`);
return result;
} catch (error) {
logger.error(
`배치 테이블 정보 조회 실패: connectionId=${connectionId}, error=${
error instanceof Error ? error.message : error
}`
);
throw error;
}
}
/**
* 커넥션별 컬럼 정보 조회
*/
async getColumnsFromConnection(
connectionId: number,
tableName: string
): Promise<ColumnInfo[]> {
try {
logger.info(
`컬럼 정보 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
// connectionId가 0이면 메인 DB의 컬럼 정보 반환
if (connectionId === 0) {
console.log(`🔍 메인 DB 컬럼 정보 조회 시작: ${tableName}`);
const columnsResult = await this.tableManagementService.getColumnList(
tableName,
1,
1000
);
console.log(
`✅ 메인 DB 컬럼 조회 성공: ${columnsResult.columns.length}`
);
// 디버깅: inputType이 'code'인 컬럼들 확인
const codeColumns = columnsResult.columns.filter(
(col) => col.inputType === "code"
);
console.log(
"🔍 메인 DB 코드 타입 컬럼들:",
codeColumns.map((col) => ({
columnName: col.columnName,
inputType: col.inputType,
webType: col.webType,
codeCategory: col.codeCategory,
}))
);
const mappedColumns = columnsResult.columns.map((column) => ({
columnName: column.columnName,
displayName: column.displayName || column.columnName, // 라벨이 있으면 라벨 사용, 없으면 컬럼명
dataType: column.dataType,
dbType: column.dataType, // dataType을 dbType으로 사용
webType: column.webType || "text", // webType 사용, 기본값 text
inputType: column.inputType || "direct", // column_labels의 input_type 추가
codeCategory: column.codeCategory, // 코드 카테고리 정보 추가
isNullable: column.isNullable === "Y",
isPrimaryKey: column.isPrimaryKey || false,
defaultValue: column.defaultValue,
maxLength: column.maxLength,
description: column.description,
connectionId: 0, // 메인 DB 구분용
}));
// 디버깅: 매핑된 컬럼 정보 확인
console.log(
"🔍 매핑된 컬럼 정보 샘플:",
mappedColumns.slice(0, 3).map((col) => ({
columnName: col.columnName,
inputType: col.inputType,
webType: col.webType,
connectionId: col.connectionId,
}))
);
// status 컬럼 특별 확인
const statusColumn = mappedColumns.find(
(col) => col.columnName === "status"
);
if (statusColumn) {
console.log("🔍 status 컬럼 상세 정보:", statusColumn);
}
return mappedColumns;
}
// 외부 DB 연결 정보 가져오기
const connectionResult =
await ExternalDbConnectionService.getConnectionById(connectionId);
if (!connectionResult.success || !connectionResult.data) {
throw new Error(`커넥션을 찾을 수 없습니다: ${connectionId}`);
}
const connection = connectionResult.data;
// 외부 DB의 컬럼 정보 조회
console.log(
`🔍 외부 DB 컬럼 정보 조회 시작: connectionId=${connectionId}, table=${tableName}`
);
const columnsResult = await ExternalDbConnectionService.getTableColumns(
connectionId,
tableName
);
if (!columnsResult.success || !columnsResult.data) {
console.error(`❌ 외부 DB 컬럼 조회 실패: ${columnsResult.message}`);
throw new Error(columnsResult.message || "컬럼 조회 실패");
}
const columns = columnsResult.data;
console.log(`✅ 외부 DB 컬럼 조회 성공: ${columns.length}`);
// MSSQL 컬럼 데이터 구조 디버깅
if (columns.length > 0) {
console.log(
`🔍 MSSQL 컬럼 데이터 구조 분석:`,
JSON.stringify(columns[0], null, 2)
);
console.log(`🔍 모든 컬럼 키들:`, Object.keys(columns[0]));
}
return columns.map((column: any) => {
// MSSQL과 PostgreSQL 데이터 타입 필드명이 다를 수 있음
// MSSQL: name, type, description (MSSQLConnector에서 alias로 지정)
// PostgreSQL: column_name, data_type, column_comment
const dataType =
column.type || // MSSQL (MSSQLConnector alias)
column.data_type || // PostgreSQL
column.DATA_TYPE ||
column.Type ||
column.dataType ||
column.column_type ||
column.COLUMN_TYPE ||
"unknown";
const columnName =
column.name || // MSSQL (MSSQLConnector alias)
column.column_name || // PostgreSQL
column.COLUMN_NAME ||
column.Name ||
column.columnName ||
column.COLUMN_NAME;
const columnComment =
column.description || // MSSQL (MSSQLConnector alias)
column.column_comment || // PostgreSQL
column.COLUMN_COMMENT ||
column.Description ||
column.comment;
console.log(`🔍 컬럼 매핑: ${columnName} - 타입: ${dataType}`);
return {
columnName: columnName,
displayName: columnComment || columnName, // 라벨(comment)이 있으면 라벨 사용, 없으면 컬럼명
dataType: dataType,
dbType: dataType,
webType: this.mapDataTypeToWebType(dataType),
inputType: "direct", // 외부 DB는 항상 direct (코드 타입 없음)
isNullable:
column.nullable === "YES" || // MSSQL (MSSQLConnector alias)
column.is_nullable === "YES" || // PostgreSQL
column.IS_NULLABLE === "YES" ||
column.Nullable === true,
isPrimaryKey: column.is_primary_key || column.IS_PRIMARY_KEY || false,
defaultValue:
column.default_value || // MSSQL (MSSQLConnector alias)
column.column_default || // PostgreSQL
column.COLUMN_DEFAULT,
maxLength:
column.max_length || // MSSQL (MSSQLConnector alias)
column.character_maximum_length || // PostgreSQL
column.CHARACTER_MAXIMUM_LENGTH,
connectionId: connectionId, // 외부 DB 구분용
description: columnComment,
};
});
} catch (error) {
logger.error(`컬럼 정보 조회 실패: ${error}`);
throw new Error(
`컬럼 정보 조회 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 🆕 자기 자신 테이블 작업 전용 검증
*/
async validateSelfTableOperation(
tableName: string,
operation: "update" | "delete",
conditions: any[]
): Promise<ValidationResult> {
try {
logger.info(
`자기 자신 테이블 작업 검증: table=${tableName}, operation=${operation}`
);
const warnings: string[] = [];
// 1. 기본 조건 체크
if (!conditions || conditions.length === 0) {
return {
isValid: false,
error: `자기 자신 테이블 ${operation.toUpperCase()} 작업에는 반드시 조건이 필요합니다.`,
};
}
// 2. DELETE 작업에 대한 추가 검증
if (operation === "delete") {
// 부정 조건 체크
const hasNegativeConditions = conditions.some((condition) => {
const conditionStr = JSON.stringify(condition).toLowerCase();
return (
conditionStr.includes("!=") ||
conditionStr.includes("not in") ||
conditionStr.includes("not exists")
);
});
if (hasNegativeConditions) {
return {
isValid: false,
error:
"자기 자신 테이블 삭제 시 부정 조건(!=, NOT IN, NOT EXISTS)은 위험합니다.",
};
}
// 조건 개수 체크
if (conditions.length < 2) {
warnings.push(
"자기 자신 테이블 삭제 시 WHERE 조건을 2개 이상 설정하는 것을 권장합니다."
);
}
}
// 3. UPDATE 작업에 대한 추가 검증
if (operation === "update") {
warnings.push("자기 자신 테이블 업데이트 시 무한 루프에 주의하세요.");
}
return {
isValid: true,
warnings: warnings.length > 0 ? warnings : undefined,
};
} catch (error) {
logger.error(`자기 자신 테이블 작업 검증 실패: ${error}`);
return {
isValid: false,
error: `검증 과정에서 오류가 발생했습니다: ${error instanceof Error ? error.message : error}`,
};
}
}
/**
* 🆕 메인 DB 작업 (connectionId = 0인 경우)
*/
async executeOnMainDatabase(
operation: "select" | "insert" | "update" | "delete",
tableName: string,
data?: Record<string, any>,
conditions?: Record<string, any>
): Promise<any> {
try {
logger.info(
`메인 DB 작업 실행: operation=${operation}, table=${tableName}`
);
switch (operation) {
case "select":
let query = `SELECT * FROM ${tableName}`;
const queryParams: any[] = [];
if (conditions && Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ");
query += ` WHERE ${whereClause}`;
queryParams.push(...Object.values(conditions));
}
return await prisma.$queryRawUnsafe(query, ...queryParams);
case "insert":
if (!data) throw new Error("INSERT 작업에는 데이터가 필요합니다.");
const insertColumns = Object.keys(data);
const insertValues = Object.values(data);
const insertPlaceholders = insertValues
.map((_, index) => `$${index + 1}`)
.join(", ");
const insertQuery = `
INSERT INTO ${tableName} (${insertColumns.join(", ")})
VALUES (${insertPlaceholders})
RETURNING *
`;
const insertResult = await prisma.$queryRawUnsafe(
insertQuery,
...insertValues
);
return Array.isArray(insertResult) ? insertResult[0] : insertResult;
case "update":
if (!data) throw new Error("UPDATE 작업에는 데이터가 필요합니다.");
if (!conditions)
throw new Error("UPDATE 작업에는 조건이 필요합니다.");
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const updateWhereClause = Object.keys(conditions)
.map(
(key, index) =>
`${key} = $${Object.keys(data).length + index + 1}`
)
.join(" AND ");
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE ${updateWhereClause}
RETURNING *
`;
const updateParams = [
...Object.values(data),
...Object.values(conditions),
];
return await prisma.$queryRawUnsafe(updateQuery, ...updateParams);
case "delete":
if (!conditions)
throw new Error("DELETE 작업에는 조건이 필요합니다.");
const deleteWhereClause = Object.keys(conditions)
.map((key, index) => `${key} = $${index + 1}`)
.join(" AND ");
const deleteQuery = `
DELETE FROM ${tableName}
WHERE ${deleteWhereClause}
RETURNING *
`;
return await prisma.$queryRawUnsafe(
deleteQuery,
...Object.values(conditions)
);
default:
throw new Error(`지원하지 않는 작업입니다: ${operation}`);
}
} catch (error) {
logger.error(`메인 DB 작업 실패: ${error}`);
throw new Error(
`메인 DB 작업 실패: ${error instanceof Error ? error.message : error}`
);
}
}
/**
* 데이터 타입을 웹 타입으로 매핑
*/
private mapDataTypeToWebType(dataType: string | undefined | null): string {
// 안전한 타입 검사
if (!dataType || typeof dataType !== "string") {
console.warn(`⚠️ 잘못된 데이터 타입: ${dataType}, 기본값 'text' 사용`);
return "text";
}
const lowerType = dataType.toLowerCase();
// PostgreSQL & MSSQL 타입 매핑
if (
lowerType.includes("int") ||
lowerType.includes("serial") ||
lowerType.includes("bigint")
) {
return "number";
}
if (
lowerType.includes("decimal") ||
lowerType.includes("numeric") ||
lowerType.includes("float") ||
lowerType.includes("money") ||
lowerType.includes("real")
) {
return "decimal";
}
if (lowerType.includes("date") && !lowerType.includes("time")) {
return "date";
}
if (
lowerType.includes("timestamp") ||
lowerType.includes("datetime") ||
lowerType.includes("datetime2")
) {
return "datetime";
}
if (lowerType.includes("bool") || lowerType.includes("bit")) {
return "boolean";
}
if (
lowerType.includes("text") ||
lowerType.includes("clob") ||
lowerType.includes("ntext")
) {
return "textarea";
}
// MSSQL 특수 타입들
if (
lowerType.includes("varchar") ||
lowerType.includes("nvarchar") ||
lowerType.includes("char")
) {
return "text";
}
return "text";
}
}