ERP-node/PHASE3.11_DDL_AUDIT_LOGGER_...

408 lines
10 KiB
Markdown
Raw Normal View History

# 📋 Phase 3.11: DDLAuditLogger Raw Query 전환 계획
## 📋 개요
DDLAuditLogger는 **8개의 Prisma 호출**이 있으며, DDL 실행 감사 로그 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | --------------------------------------------- |
| 파일 위치 | `backend-node/src/services/ddlAuditLogger.ts` |
| 파일 크기 | 350 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **8/8 (100%)****전환 완료** |
| 복잡도 | 중간 (통계 쿼리, $executeRaw) |
| 우선순위 | 🟡 중간 (Phase 3.11) |
| **상태** | ✅ **완료** |
### 🎯 전환 목표
-**8개 모든 Prisma 호출을 `db.ts`의 `query()`, `queryOne()` 함수로 교체**
- ⏳ DDL 감사 로그 기능 정상 동작
- ⏳ 통계 쿼리 전환 (GROUP BY, COUNT, ORDER BY)
- ⏳ $executeRaw → query 전환
- ⏳ $queryRawUnsafe → query 전환
- ⏳ 동적 WHERE 조건 생성
- ⏳ TypeScript 컴파일 성공
-**Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (8개)
#### 1. **logDDLStart()** - DDL 시작 로그 (INSERT)
```typescript
// Line 27
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
```
#### 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(
`SELECT
COUNT(*) as total_executions,
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) as avg_duration
FROM ddl_audit_logs
WHERE ${whereClause}`
)) as any[];
// Line 212 - DDL 타입별 통계
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
// Line 224 - 사용자별 통계
const userStats = (await prisma.$queryRawUnsafe(
`SELECT executed_by, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY executed_by
ORDER BY count DESC
LIMIT 10`
)) as any[];
// Line 237 - 최근 실패 로그
const recentFailures = (await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE status = 'failed' AND ${whereClause}
ORDER BY started_at DESC
LIMIT 5`
)) as any[];
```
#### 4. **getExecutionHistory()** - 실행 이력 조회
```typescript
// Line 287
const history = await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE table_name = $1 AND company_code = $2
ORDER BY started_at DESC
LIMIT $3`,
tableName,
companyCode,
limit
);
```
#### 5. **cleanupOldLogs()** - 오래된 로그 삭제
```typescript
// Line 320
const result = await prisma.$executeRaw`
DELETE FROM ddl_audit_logs
WHERE started_at < NOW() - INTERVAL '${retentionDays} days'
AND company_code = ${companyCode}
`;
```
---
## 💡 전환 전략
### 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
---
## 💻 전환 예시
### 예시 1: $executeRaw → query (INSERT)
**변경 전**:
```typescript
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
```
**변경 후**:
```typescript
await query(
`INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7::jsonb)`,
[
executionId,
ddlType,
tableName,
"in_progress",
executedBy,
companyCode,
JSON.stringify(metadata),
]
);
```
### 예시 2: 동적 WHERE 조건
**변경 전**:
```typescript
let query = `SELECT * FROM ddl_audit_logs WHERE 1=1`;
const params: any[] = [];
if (filters.ddlType) {
query += ` AND ddl_type = ?`;
params.push(filters.ddlType);
}
const logs = await prisma.$queryRawUnsafe(query, ...params);
```
**변경 후**:
```typescript
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (filters.ddlType) {
conditions.push(`ddl_type = $${paramIndex++}`);
params.push(filters.ddlType);
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM ddl_audit_logs ${whereClause}`;
const logs = await query<any>(sql, params);
```
### 예시 3: 통계 쿼리 (GROUP BY)
**변경 전**:
```typescript
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
```
**변경 후**:
```typescript
const ddlTypeStats = await query<{ ddl_type: string; count: string }>(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`,
params
);
```
---
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
`metadata` 필드는 JSONB 타입으로, INSERT 시 `::jsonb` 캐스팅 필요:
```typescript
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
- executedBy
- dateRange (startDate, endDate)
---
## ✅ 전환 완료 내역
### 전환된 Prisma 호출 (8개)
1. **`logDDLExecution()`** - DDL 실행 로그 INSERT
- Before: `prisma.$executeRaw`
- After: `query()` with 7 parameters
2. **`getAuditLogs()`** - 감사 로그 목록 조회
- Before: `prisma.$queryRawUnsafe`
- After: `query<any>()` with dynamic WHERE clause
3. **`getDDLStatistics()`** - 통계 조회 (4개 쿼리)
- Before: 4x `prisma.$queryRawUnsafe`
- After: 4x `query<any>()`
- totalStats: 전체 실행 통계 (CASE WHEN 집계)
- ddlTypeStats: DDL 타입별 통계 (GROUP BY)
- userStats: 사용자별 통계 (GROUP BY, LIMIT 10)
- recentFailures: 최근 실패 로그 (WHERE success = false)
4. **`getTableDDLHistory()`** - 테이블별 DDL 히스토리
- Before: `prisma.$queryRawUnsafe`
- After: `query<any>()` 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 호출 전환 (✅ 완료)
- [ ] `logDDLStart()` - INSERT ($executeRaw → query)
- [ ] `logDDLComplete()` - UPDATE (이미 query 사용 중일 가능성)
- [ ] `logDDLError()` - UPDATE (이미 query 사용 중일 가능성)
- [ ] `getAuditLogs()` - SELECT with filters ($queryRawUnsafe → query)
- [ ] `getAuditStats()` 내 4개 쿼리:
- [ ] totalStats (집계 쿼리)
- [ ] ddlTypeStats (GROUP BY)
- [ ] userStats (GROUP BY + LIMIT)
- [ ] recentFailures (필터 + ORDER BY + LIMIT)
- [ ] `getExecutionHistory()` - SELECT with params ($queryRawUnsafe → query)
- [ ] `cleanupOldLogs()` - DELETE ($executeRaw → query)
### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] Prisma import 완전 제거
- [ ] 타입 정의 확인
### 3단계: 테스트
- [ ] 단위 테스트 작성 (8개)
- [ ] DDL 시작 로그 테스트
- [ ] DDL 완료 로그 테스트
- [ ] 감사 로그 목록 조회 테스트
- [ ] 통계 조회 테스트
- [ ] 실행 이력 조회 테스트
- [ ] 오래된 로그 삭제 테스트
- [ ] 통합 테스트 작성 (3개)
- [ ] 전체 DDL 실행 플로우 테스트
- [ ] 필터링 및 페이징 테스트
- [ ] 통계 정확성 테스트
- [ ] 성능 테스트
- [ ] 대량 로그 조회 성능
- [ ] 통계 쿼리 성능
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
- [ ] 주요 변경사항 기록
- [ ] 성능 벤치마크 결과
---
## 🎯 예상 난이도 및 소요 시간
- **난이도**: ⭐⭐⭐ (중간)
- 복잡한 통계 쿼리 (GROUP BY, CASE WHEN)
- 동적 WHERE 조건 생성
- JSON 필드 처리
- **예상 소요 시간**: 1~1.5시간
- Prisma 호출 전환: 30분
- 테스트: 20분
- 문서화: 10분
---
## 📌 참고사항
### 관련 서비스
- `DDLExecutionService` - DDL 실행 (이미 전환 완료)
- `DDLSafetyValidator` - DDL 안전성 검증
### 의존성
- `../database/db` - query, queryOne 함수
- `../types/ddl` - DDL 관련 타입
- `../utils/logger` - 로깅
---
**상태**: ⏳ **대기 중**
**특이사항**: 통계 쿼리, JSON 필드, 동적 WHERE 조건 포함