803 lines
25 KiB
TypeScript
803 lines
25 KiB
TypeScript
|
|
/**
|
||
|
|
* 다중 커넥션 쿼리 실행 서비스
|
||
|
|
* 외부 데이터베이스 커넥션을 통한 CRUD 작업 지원
|
||
|
|
* 자기 자신 테이블 작업을 위한 안전장치 포함
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { ExternalDbConnectionService } from "./externalDbConnectionService";
|
||
|
|
import { TableManagementService } from "./tableManagementService";
|
||
|
|
import { ExternalDbConnection } from "../types/externalDbTypes";
|
||
|
|
import { ColumnTypeInfo, TableInfo } from "../types/tableManagement";
|
||
|
|
import { PrismaClient } from "@prisma/client";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
const prisma = new PrismaClient();
|
||
|
|
|
||
|
|
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 쿼리 구성
|
||
|
|
const columns = Object.keys(data);
|
||
|
|
const values = Object.values(data);
|
||
|
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
INSERT INTO ${tableName} (${columns.join(", ")})
|
||
|
|
VALUES (${placeholders})
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
|
||
|
|
// 외부 DB에서 쿼리 실행
|
||
|
|
const result = await ExternalDbConnectionService.executeQuery(
|
||
|
|
connectionId,
|
||
|
|
query
|
||
|
|
);
|
||
|
|
|
||
|
|
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 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}개`
|
||
|
|
);
|
||
|
|
|
||
|
|
return 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
|
||
|
|
isNullable: column.isNullable === "Y",
|
||
|
|
isPrimaryKey: column.isPrimaryKey || false,
|
||
|
|
defaultValue: column.defaultValue,
|
||
|
|
maxLength: column.maxLength,
|
||
|
|
description: column.description,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 외부 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),
|
||
|
|
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,
|
||
|
|
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";
|
||
|
|
}
|
||
|
|
}
|