# ๐Ÿ“‹ 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(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()` 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 ํ˜ธ์ถœ ์ „ํ™˜ (โœ… ์™„๋ฃŒ) - [ ] `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 ์กฐ๊ฑด ํฌํ•จ