/** * 다중 커넥션 쿼리 실행 서비스 * 외부 데이터베이스 커넥션을 통한 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 ): Promise[]> { 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 ): Promise { 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, conditions: Record ): Promise { 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, maxDeleteCount: number = 100 ): Promise { 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 { 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 | undefined; let retryCount = 0; const maxRetries = 2; while (retryCount <= maxRetries) { try { columnsResult = (await Promise.race([ ExternalDbConnectionService.getTableColumns( connectionId, tableName ), new Promise>((_, reject) => setTimeout( () => reject(new Error("컬럼 조회 타임아웃 (15초)")), 15000 ) ), ])) as ApiResponse; 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 { 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 { 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, conditions?: Record ): Promise { 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"; } }