feat: Phase 2.5 ExternalDbConnectionService Raw Query 전환 완료

- 15개 Prisma 호출을 모두 Raw Query로 전환
- 동적 WHERE 조건 생성 구현 (ILIKE 검색 지원)
- 동적 UPDATE 쿼리 구현 (변경된 필드만 업데이트)
- 비밀번호 암호화/복호화 로직 유지
- TypeScript 컴파일 성공 (linter 에러 0개)
- Prisma import 완전 제거

전환된 주요 함수:
- getConnections() - 외부 DB 연결 목록 조회
- createConnection() - 새 연결 생성 + 중복 확인
- updateConnection() - 연결 정보 수정
- deleteConnection() - 연결 삭제
- testConnectionById() - 연결 테스트
- getTables() - 테이블 목록 조회

Phase 2 진행률: 131/162 (80.9%)
전체 진행률: 217/444 (48.9%)
This commit is contained in:
kjs
2025-10-01 10:11:19 +09:00
parent 57f1d8274e
commit 5f3f869135
3 changed files with 240 additions and 151 deletions

View File

@@ -1,7 +1,7 @@
// 외부 DB 연결 서비스
// 작성일: 2024-12-17
import prisma from "../config/database";
import { query, queryOne } from "../database/db";
import {
ExternalDbConnection,
ExternalDbConnectionFilter,
@@ -20,43 +20,47 @@ export class ExternalDbConnectionService {
filter: ExternalDbConnectionFilter
): Promise<ApiResponse<ExternalDbConnection[]>> {
try {
const where: any = {};
// WHERE 조건 동적 생성
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
// 필터 조건 적용
if (filter.db_type) {
where.db_type = filter.db_type;
whereConditions.push(`db_type = $${paramIndex++}`);
params.push(filter.db_type);
}
if (filter.is_active) {
where.is_active = filter.is_active;
whereConditions.push(`is_active = $${paramIndex++}`);
params.push(filter.is_active);
}
if (filter.company_code) {
where.company_code = filter.company_code;
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
}
// 검색 조건 적용 (연결명 또는 설명에서 검색)
if (filter.search && filter.search.trim()) {
where.OR = [
{
connection_name: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
{
description: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
];
whereConditions.push(
`(connection_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
params.push(`%${filter.search.trim()}%`);
paramIndex++;
}
const connections = await prisma.external_db_connections.findMany({
where,
orderBy: [{ is_active: "desc" }, { connection_name: "asc" }],
});
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const connections = await query<any>(
`SELECT * FROM external_db_connections
${whereClause}
ORDER BY is_active DESC, connection_name ASC`,
params
);
// 비밀번호는 반환하지 않음 (보안)
const safeConnections = connections.map((conn) => ({
@@ -89,26 +93,25 @@ export class ExternalDbConnectionService {
try {
// 기본 연결 목록 조회
const connectionsResult = await this.getConnections(filter);
if (!connectionsResult.success || !connectionsResult.data) {
return {
success: false,
message: "연결 목록 조회에 실패했습니다."
message: "연결 목록 조회에 실패했습니다.",
};
}
// DB 타입 카테고리 정보 조회
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
const categories = await query<any>(
`SELECT * FROM db_type_categories
WHERE is_active = true
ORDER BY sort_order ASC, display_name ASC`,
[]
);
// DB 타입별로 그룹화
const groupedConnections: Record<string, any> = {};
// 카테고리 정보를 포함한 그룹 초기화
categories.forEach((category: any) => {
groupedConnections[category.type_code] = {
@@ -117,36 +120,36 @@ export class ExternalDbConnectionService {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order
sort_order: category.sort_order,
},
connections: []
connections: [],
};
});
// 연결을 해당 타입 그룹에 배치
connectionsResult.data.forEach(connection => {
connectionsResult.data.forEach((connection) => {
if (groupedConnections[connection.db_type]) {
groupedConnections[connection.db_type].connections.push(connection);
} else {
// 카테고리에 없는 DB 타입인 경우 기타 그룹에 추가
if (!groupedConnections['other']) {
groupedConnections['other'] = {
if (!groupedConnections["other"]) {
groupedConnections["other"] = {
category: {
type_code: 'other',
display_name: '기타',
icon: 'database',
color: '#6B7280',
sort_order: 999
type_code: "other",
display_name: "기타",
icon: "database",
color: "#6B7280",
sort_order: 999,
},
connections: []
connections: [],
};
}
groupedConnections['other'].connections.push(connection);
groupedConnections["other"].connections.push(connection);
}
});
// 연결이 없는 빈 그룹 제거
Object.keys(groupedConnections).forEach(key => {
Object.keys(groupedConnections).forEach((key) => {
if (groupedConnections[key].connections.length === 0) {
delete groupedConnections[key];
}
@@ -155,14 +158,14 @@ export class ExternalDbConnectionService {
return {
success: true,
data: groupedConnections,
message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`
message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`,
};
} catch (error) {
console.error("그룹화된 연결 목록 조회 실패:", error);
return {
success: false,
message: "그룹화된 연결 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
error: error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
@@ -174,9 +177,10 @@ export class ExternalDbConnectionService {
id: number
): Promise<ApiResponse<ExternalDbConnection>> {
try {
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
if (!connection) {
return {
@@ -214,9 +218,10 @@ export class ExternalDbConnectionService {
id: number
): Promise<ApiResponse<ExternalDbConnection>> {
try {
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
if (!connection) {
return {
@@ -257,13 +262,11 @@ export class ExternalDbConnectionService {
this.validateConnectionData(data);
// 연결명 중복 확인
const existingConnection = await prisma.external_db_connections.findFirst(
{
where: {
connection_name: data.connection_name,
company_code: data.company_code,
},
}
const existingConnection = await queryOne(
`SELECT id FROM external_db_connections
WHERE connection_name = $1 AND company_code = $2
LIMIT 1`,
[data.connection_name, data.company_code]
);
if (existingConnection) {
@@ -276,30 +279,35 @@ export class ExternalDbConnectionService {
// 비밀번호 암호화
const encryptedPassword = PasswordEncryption.encrypt(data.password);
const newConnection = await prisma.external_db_connections.create({
data: {
connection_name: data.connection_name,
description: data.description,
db_type: data.db_type,
host: data.host,
port: data.port,
database_name: data.database_name,
username: data.username,
password: encryptedPassword,
connection_timeout: data.connection_timeout,
query_timeout: data.query_timeout,
max_connections: data.max_connections,
ssl_enabled: data.ssl_enabled,
ssl_cert_path: data.ssl_cert_path,
connection_options: data.connection_options as any,
company_code: data.company_code,
is_active: data.is_active,
created_by: data.created_by,
updated_by: data.updated_by,
created_date: new Date(),
updated_date: new Date(),
},
});
const newConnection = await queryOne<any>(
`INSERT INTO external_db_connections (
connection_name, description, db_type, host, port, database_name,
username, password, connection_timeout, query_timeout, max_connections,
ssl_enabled, ssl_cert_path, connection_options, company_code, is_active,
created_by, updated_by, created_date, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW(), NOW())
RETURNING *`,
[
data.connection_name,
data.description,
data.db_type,
data.host,
data.port,
data.database_name,
data.username,
encryptedPassword,
data.connection_timeout,
data.query_timeout,
data.max_connections,
data.ssl_enabled,
data.ssl_cert_path,
JSON.stringify(data.connection_options),
data.company_code,
data.is_active,
data.created_by,
data.updated_by,
]
);
// 비밀번호는 반환하지 않음
const safeConnection = {
@@ -332,10 +340,10 @@ export class ExternalDbConnectionService {
): Promise<ApiResponse<ExternalDbConnection>> {
try {
// 기존 연결 확인
const existingConnection =
await prisma.external_db_connections.findUnique({
where: { id },
});
const existingConnection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
if (!existingConnection) {
return {
@@ -346,15 +354,18 @@ export class ExternalDbConnectionService {
// 연결명 중복 확인 (자신 제외)
if (data.connection_name) {
const duplicateConnection =
await prisma.external_db_connections.findFirst({
where: {
connection_name: data.connection_name,
company_code:
data.company_code || existingConnection.company_code,
id: { not: id },
},
});
const duplicateConnection = await queryOne(
`SELECT id FROM external_db_connections
WHERE connection_name = $1
AND company_code = $2
AND id != $3
LIMIT 1`,
[
data.connection_name,
data.company_code || existingConnection.company_code,
id,
]
);
if (duplicateConnection) {
return {
@@ -406,23 +417,59 @@ export class ExternalDbConnectionService {
}
// 업데이트 데이터 준비
const updateData: any = {
...data,
updated_date: new Date(),
};
const updates: string[] = [];
const updateParams: any[] = [];
let paramIndex = 1;
// 각 필드를 동적으로 추가
const fields = [
"connection_name",
"description",
"db_type",
"host",
"port",
"database_name",
"username",
"connection_timeout",
"query_timeout",
"max_connections",
"ssl_enabled",
"ssl_cert_path",
"connection_options",
"company_code",
"is_active",
"updated_by",
];
for (const field of fields) {
if (data[field as keyof ExternalDbConnection] !== undefined) {
updates.push(`${field} = $${paramIndex++}`);
const value = data[field as keyof ExternalDbConnection];
updateParams.push(
field === "connection_options" ? JSON.stringify(value) : value
);
}
}
// 비밀번호가 변경된 경우 암호화 (연결 테스트 통과 후)
if (data.password && data.password !== "***ENCRYPTED***") {
updateData.password = PasswordEncryption.encrypt(data.password);
} else {
// 비밀번호 필드 제거 (변경하지 않음)
delete updateData.password;
updates.push(`password = $${paramIndex++}`);
updateParams.push(PasswordEncryption.encrypt(data.password));
}
const updatedConnection = await prisma.external_db_connections.update({
where: { id },
data: updateData,
});
// updated_date는 항상 업데이트
updates.push(`updated_date = NOW()`);
// id 파라미터 추가
updateParams.push(id);
const updatedConnection = await queryOne<any>(
`UPDATE external_db_connections
SET ${updates.join(", ")}
WHERE id = $${paramIndex}
RETURNING *`,
updateParams
);
// 비밀번호는 반환하지 않음
const safeConnection = {
@@ -451,10 +498,10 @@ export class ExternalDbConnectionService {
*/
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
try {
const existingConnection =
await prisma.external_db_connections.findUnique({
where: { id },
});
const existingConnection = await queryOne(
`SELECT id FROM external_db_connections WHERE id = $1`,
[id]
);
if (!existingConnection) {
return {
@@ -464,9 +511,7 @@ export class ExternalDbConnectionService {
}
// 물리 삭제 (실제 데이터 삭제)
await prisma.external_db_connections.delete({
where: { id },
});
await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]);
return {
success: true,
@@ -491,9 +536,10 @@ export class ExternalDbConnectionService {
): Promise<import("../types/externalDbTypes").ConnectionTestResult> {
try {
// 저장된 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
if (!connection) {
return {
@@ -674,10 +720,10 @@ export class ExternalDbConnectionService {
*/
static async getDecryptedPassword(id: number): Promise<string | null> {
try {
const connection = await prisma.external_db_connections.findUnique({
where: { id },
select: { password: true },
});
const connection = await queryOne<{ password: string }>(
`SELECT password FROM external_db_connections WHERE id = $1`,
[id]
);
if (!connection) {
return null;
@@ -701,9 +747,10 @@ export class ExternalDbConnectionService {
try {
// 연결 정보 조회
console.log("연결 정보 조회 시작:", { id });
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
console.log("조회된 연결 정보:", connection);
if (!connection) {
@@ -753,14 +800,25 @@ export class ExternalDbConnectionService {
let result;
try {
const dbType = connection.db_type?.toLowerCase() || 'postgresql';
const dbType = connection.db_type?.toLowerCase() || "postgresql";
// 파라미터 바인딩을 지원하는 DB 타입들
const supportedDbTypes = ['oracle', 'mysql', 'mariadb', 'postgresql', 'sqlite', 'sqlserver', 'mssql'];
const supportedDbTypes = [
"oracle",
"mysql",
"mariadb",
"postgresql",
"sqlite",
"sqlserver",
"mssql",
];
if (supportedDbTypes.includes(dbType) && params.length > 0) {
// 파라미터 바인딩 지원 DB: 안전한 파라미터 바인딩 사용
logger.info(`${dbType.toUpperCase()} 파라미터 바인딩 실행:`, { query, params });
logger.info(`${dbType.toUpperCase()} 파라미터 바인딩 실행:`, {
query,
params,
});
result = await (connector as any).executeQuery(query, params);
} else {
// 파라미터가 없거나 지원하지 않는 DB: 기본 방식 사용
@@ -846,9 +904,10 @@ export class ExternalDbConnectionService {
static async getTables(id: number): Promise<ApiResponse<TableInfo[]>> {
try {
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
if (!connection) {
return {