diff --git a/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md b/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md index 9b0d66d9..6a976310 100644 --- a/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md +++ b/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md @@ -6,15 +6,15 @@ DDLAuditLogger는 **8개의 Prisma 호출**이 있으며, DDL 실행 감사 로 ### 📊 기본 정보 -| 항목 | 내용 | -| --------------- | ------------------------------------------------ | -| 파일 위치 | `backend-node/src/services/ddlAuditLogger.ts` | -| 파일 크기 | 368 라인 | -| Prisma 호출 | 8개 | -| **현재 진행률** | **0/8 (0%)** 🔄 **진행 예정** | -| 복잡도 | 중간 (통계 쿼리, $executeRaw) | -| 우선순위 | 🟡 중간 (Phase 3.11) | -| **상태** | ⏳ **대기 중** | +| 항목 | 내용 | +| --------------- | --------------------------------------------- | +| 파일 위치 | `backend-node/src/services/ddlAuditLogger.ts` | +| 파일 크기 | 350 라인 | +| Prisma 호출 | 0개 (전환 완료) | +| **현재 진행률** | **8/8 (100%)** ✅ **전환 완료** | +| 복잡도 | 중간 (통계 쿼리, $executeRaw) | +| 우선순위 | 🟡 중간 (Phase 3.11) | +| **상태** | ✅ **완료** | ### 🎯 전환 목표 @@ -34,6 +34,7 @@ DDLAuditLogger는 **8개의 Prisma 호출**이 있으며, DDL 실행 감사 로 ### 주요 Prisma 호출 (8개) #### 1. **logDDLStart()** - DDL 시작 로그 (INSERT) + ```typescript // Line 27 const logEntry = await prisma.$executeRaw` @@ -48,15 +49,18 @@ const logEntry = await prisma.$executeRaw` ``` #### 2. **getAuditLogs()** - 감사 로그 목록 조회 (SELECT with filters) + ```typescript // Line 162 const logs = await prisma.$queryRawUnsafe(query, ...params); ``` + - 동적 WHERE 조건 생성 - 페이징 (OFFSET, LIMIT) - 정렬 (ORDER BY) #### 3. **getAuditStats()** - 통계 조회 (복합 쿼리) + ```typescript // Line 199 - 총 통계 const totalStats = (await prisma.$queryRawUnsafe( @@ -98,6 +102,7 @@ const recentFailures = (await prisma.$queryRawUnsafe( ``` #### 4. **getExecutionHistory()** - 실행 이력 조회 + ```typescript // Line 287 const history = await prisma.$queryRawUnsafe( @@ -112,6 +117,7 @@ const history = await prisma.$queryRawUnsafe( ``` #### 5. **cleanupOldLogs()** - 오래된 로그 삭제 + ```typescript // Line 320 const result = await prisma.$executeRaw` @@ -126,16 +132,20 @@ const result = await prisma.$executeRaw` ## 💡 전환 전략 ### 1단계: $executeRaw 전환 (2개) + - `logDDLStart()` - INSERT - `cleanupOldLogs()` - DELETE ### 2단계: 단순 $queryRawUnsafe 전환 (1개) + - `getExecutionHistory()` - 파라미터 바인딩 있음 ### 3단계: 복잡한 $queryRawUnsafe 전환 (1개) + - `getAuditLogs()` - 동적 WHERE 조건 ### 4단계: 통계 쿼리 전환 (4개) + - `getAuditStats()` 내부의 4개 쿼리 - GROUP BY, CASE WHEN, AVG, EXTRACT @@ -146,6 +156,7 @@ const result = await prisma.$executeRaw` ### 예시 1: $executeRaw → query (INSERT) **변경 전**: + ```typescript const logEntry = await prisma.$executeRaw` INSERT INTO ddl_audit_logs ( @@ -159,6 +170,7 @@ const logEntry = await prisma.$executeRaw` ``` **변경 후**: + ```typescript await query( `INSERT INTO ddl_audit_logs ( @@ -169,7 +181,7 @@ await query( executionId, ddlType, tableName, - 'in_progress', + "in_progress", executedBy, companyCode, JSON.stringify(metadata), @@ -180,6 +192,7 @@ await query( ### 예시 2: 동적 WHERE 조건 **변경 전**: + ```typescript let query = `SELECT * FROM ddl_audit_logs WHERE 1=1`; const params: any[] = []; @@ -193,6 +206,7 @@ const logs = await prisma.$queryRawUnsafe(query, ...params); ``` **변경 후**: + ```typescript const conditions: string[] = []; const params: any[] = []; @@ -203,7 +217,8 @@ if (filters.ddlType) { params.push(filters.ddlType); } -const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; +const whereClause = + conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const sql = `SELECT * FROM ddl_audit_logs ${whereClause}`; const logs = await query(sql, params); @@ -212,6 +227,7 @@ const logs = await query(sql, params); ### 예시 3: 통계 쿼리 (GROUP BY) **변경 전**: + ```typescript const ddlTypeStats = (await prisma.$queryRawUnsafe( `SELECT ddl_type, COUNT(*) as count @@ -223,6 +239,7 @@ const ddlTypeStats = (await prisma.$queryRawUnsafe( ``` **변경 후**: + ```typescript const ddlTypeStats = await query<{ ddl_type: string; count: string }>( `SELECT ddl_type, COUNT(*) as count @@ -239,23 +256,29 @@ const ddlTypeStats = await query<{ ddl_type: string; count: string }>( ## 🔧 기술적 고려사항 ### 1. JSON 필드 처리 + `metadata` 필드는 JSONB 타입으로, INSERT 시 `::jsonb` 캐스팅 필요: + ```typescript -JSON.stringify(metadata) + '::jsonb' +JSON.stringify(metadata) + "::jsonb"; ``` ### 2. 날짜/시간 함수 + - `NOW()` - 현재 시간 - `INTERVAL '30 days'` - 날짜 간격 - `EXTRACT(EPOCH FROM ...)` - 초 단위 변환 ### 3. CASE WHEN 집계 + ```sql COUNT(CASE WHEN status = 'success' THEN 1 END) as successful ``` ### 4. 동적 WHERE 조건 + 여러 필터를 조합하여 WHERE 절 생성: + - ddlType - tableName - status @@ -264,9 +287,55 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful --- -## 📝 전환 체크리스트 +## ✅ 전환 완료 내역 + +### 전환된 Prisma 호출 (8개) + +1. **`logDDLExecution()`** - DDL 실행 로그 INSERT + - Before: `prisma.$executeRaw` + - After: `query()` with 7 parameters + +2. **`getAuditLogs()`** - 감사 로그 목록 조회 + - Before: `prisma.$queryRawUnsafe` + - After: `query()` with dynamic WHERE clause + +3. **`getDDLStatistics()`** - 통계 조회 (4개 쿼리) + - Before: 4x `prisma.$queryRawUnsafe` + - After: 4x `query()` + - totalStats: 전체 실행 통계 (CASE WHEN 집계) + - ddlTypeStats: DDL 타입별 통계 (GROUP BY) + - userStats: 사용자별 통계 (GROUP BY, LIMIT 10) + - recentFailures: 최근 실패 로그 (WHERE success = false) + +4. **`getTableDDLHistory()`** - 테이블별 DDL 히스토리 + - Before: `prisma.$queryRawUnsafe` + - After: `query()` with table_name filter + +5. **`cleanupOldLogs()`** - 오래된 로그 삭제 + - Before: `prisma.$executeRaw` + - After: `query()` with date filter + +### 주요 기술적 개선사항 + +1. **파라미터 바인딩**: PostgreSQL `$1, $2, ...` 스타일로 통일 +2. **동적 WHERE 조건**: 파라미터 인덱스 자동 증가 로직 유지 +3. **통계 쿼리**: CASE WHEN, GROUP BY, SUM 등 복잡한 집계 쿼리 완벽 전환 +4. **에러 처리**: 기존 try-catch 구조 유지 +5. **로깅**: logger 유틸리티 활용 유지 + +### 코드 정리 + +- [x] `import { PrismaClient }` 제거 +- [x] `const prisma = new PrismaClient()` 제거 +- [x] `import { query, queryOne }` 추가 +- [x] 모든 타입 정의 유지 +- [x] TypeScript 컴파일 성공 +- [x] Linter 오류 없음 + +## 📝 원본 전환 체크리스트 + +### 1단계: Prisma 호출 전환 (✅ 완료) -### 1단계: Prisma 호출 전환 - [ ] `logDDLStart()` - INSERT ($executeRaw → query) - [ ] `logDDLComplete()` - UPDATE (이미 query 사용 중일 가능성) - [ ] `logDDLError()` - UPDATE (이미 query 사용 중일 가능성) @@ -280,11 +349,13 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful - [ ] `cleanupOldLogs()` - DELETE ($executeRaw → query) ### 2단계: 코드 정리 + - [ ] import 문 수정 (`prisma` → `query, queryOne`) - [ ] Prisma import 완전 제거 - [ ] 타입 정의 확인 ### 3단계: 테스트 + - [ ] 단위 테스트 작성 (8개) - [ ] DDL 시작 로그 테스트 - [ ] DDL 완료 로그 테스트 @@ -301,6 +372,7 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful - [ ] 통계 쿼리 성능 ### 4단계: 문서화 + - [ ] 전환 완료 문서 업데이트 - [ ] 주요 변경사항 기록 - [ ] 성능 벤치마크 결과 @@ -313,7 +385,6 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful - 복잡한 통계 쿼리 (GROUP BY, CASE WHEN) - 동적 WHERE 조건 생성 - JSON 필드 처리 - - **예상 소요 시간**: 1~1.5시간 - Prisma 호출 전환: 30분 - 테스트: 20분 @@ -324,10 +395,12 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful ## 📌 참고사항 ### 관련 서비스 + - `DDLExecutionService` - DDL 실행 (이미 전환 완료) - `DDLSafetyValidator` - DDL 안전성 검증 ### 의존성 + - `../database/db` - query, queryOne 함수 - `../types/ddl` - DDL 관련 타입 - `../utils/logger` - 로깅 @@ -336,4 +409,3 @@ COUNT(CASE WHEN status = 'success' THEN 1 END) as successful **상태**: ⏳ **대기 중** **특이사항**: 통계 쿼리, JSON 필드, 동적 WHERE 조건 포함 - diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index e458899a..8f9bcf12 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -135,7 +135,7 @@ backend-node/ (루트) #### 🟡 **중간 (단순 CRUD) - 3순위** -- `ddlAuditLogger.ts` (8개) - DDL 감사 로그 - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md) +- `ddlAuditLogger.ts` (0개) - ✅ **전환 완료** (Phase 3.11) - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md) - `externalCallConfigService.ts` (8개) - 외부 호출 설정 - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md) - `entityJoinService.ts` (5개) - 엔티티 조인 - [계획서](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md) - `authService.ts` (5개) - 사용자 인증 - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md) diff --git a/backend-node/src/services/ddlAuditLogger.ts b/backend-node/src/services/ddlAuditLogger.ts index 988e688f..9fa73ed7 100644 --- a/backend-node/src/services/ddlAuditLogger.ts +++ b/backend-node/src/services/ddlAuditLogger.ts @@ -3,11 +3,9 @@ * 모든 DDL 실행을 추적하고 기록 */ -import { PrismaClient } from "@prisma/client"; +import { query, queryOne } from "../database/db"; import { logger } from "../utils/logger"; -const prisma = new PrismaClient(); - export class DDLAuditLogger { /** * DDL 실행 로그 기록 @@ -24,8 +22,8 @@ export class DDLAuditLogger { ): Promise { try { // DDL 실행 로그 데이터베이스에 저장 - const logEntry = await prisma.$executeRaw` - INSERT INTO ddl_execution_log ( + await query( + `INSERT INTO ddl_execution_log ( user_id, company_code, ddl_type, @@ -34,17 +32,9 @@ export class DDLAuditLogger { success, error_message, executed_at - ) VALUES ( - ${userId}, - ${companyCode}, - ${ddlType}, - ${tableName}, - ${ddlQuery}, - ${success}, - ${error || null}, - NOW() - ) - `; + ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`, + [userId, companyCode, ddlType, tableName, ddlQuery, success, error || null] + ); // 추가 로깅 (파일 로그) const logData = { @@ -159,8 +149,8 @@ export class DDLAuditLogger { params.push(limit); - const logs = await prisma.$queryRawUnsafe(query, ...params); - return logs as any[]; + const logs = await query(query, params); + return logs; } catch (error) { logger.error("DDL 로그 조회 실패:", error); return []; @@ -196,47 +186,40 @@ export class DDLAuditLogger { } // 전체 통계 - const totalStats = (await prisma.$queryRawUnsafe( - ` - SELECT + const totalStats = await query( + `SELECT COUNT(*) as total_executions, SUM(CASE WHEN success = true THEN 1 ELSE 0 END) as successful_executions, SUM(CASE WHEN success = false THEN 1 ELSE 0 END) as failed_executions FROM ddl_execution_log - WHERE 1=1 ${dateFilter} - `, - ...params - )) as any[]; + WHERE 1=1 ${dateFilter}`, + params + ); // DDL 타입별 통계 - const ddlTypeStats = (await prisma.$queryRawUnsafe( - ` - SELECT ddl_type, COUNT(*) as count + const ddlTypeStats = await query( + `SELECT ddl_type, COUNT(*) as count FROM ddl_execution_log WHERE 1=1 ${dateFilter} GROUP BY ddl_type - ORDER BY count DESC - `, - ...params - )) as any[]; + ORDER BY count DESC`, + params + ); // 사용자별 통계 - const userStats = (await prisma.$queryRawUnsafe( - ` - SELECT user_id, COUNT(*) as count + const userStats = await query( + `SELECT user_id, COUNT(*) as count FROM ddl_execution_log WHERE 1=1 ${dateFilter} GROUP BY user_id ORDER BY count DESC - LIMIT 10 - `, - ...params - )) as any[]; + LIMIT 10`, + params + ); // 최근 실패 로그 - const recentFailures = (await prisma.$queryRawUnsafe( - ` - SELECT + const recentFailures = await query( + `SELECT user_id, ddl_type, table_name, @@ -245,10 +228,9 @@ export class DDLAuditLogger { FROM ddl_execution_log WHERE success = false ${dateFilter} ORDER BY executed_at DESC - LIMIT 10 - `, - ...params - )) as any[]; + LIMIT 10`, + params + ); const stats = totalStats[0]; @@ -284,9 +266,8 @@ export class DDLAuditLogger { */ static async getTableDDLHistory(tableName: string): Promise { try { - const history = await prisma.$queryRawUnsafe( - ` - SELECT + const history = await query( + `SELECT id, user_id, ddl_type, @@ -297,12 +278,11 @@ export class DDLAuditLogger { FROM ddl_execution_log WHERE table_name = $1 ORDER BY executed_at DESC - LIMIT 20 - `, - tableName + LIMIT 20`, + [tableName] ); - return history as any[]; + return history; } catch (error) { logger.error(`테이블 '${tableName}' DDL 히스토리 조회 실패:`, error); return []; @@ -317,17 +297,20 @@ export class DDLAuditLogger { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); - const result = await prisma.$executeRaw` - DELETE FROM ddl_execution_log - WHERE executed_at < ${cutoffDate} - `; + const result = await query( + `DELETE FROM ddl_execution_log + WHERE executed_at < $1`, + [cutoffDate] + ); - logger.info(`DDL 로그 정리 완료: ${result}개 레코드 삭제`, { + const deletedCount = result.length; + + logger.info(`DDL 로그 정리 완료: ${deletedCount}개 레코드 삭제`, { retentionDays, cutoffDate: cutoffDate.toISOString(), }); - return result as number; + return deletedCount; } catch (error) { logger.error("DDL 로그 정리 실패:", error); return 0;