diff --git a/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md b/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md new file mode 100644 index 00000000..9b0d66d9 --- /dev/null +++ b/PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md @@ -0,0 +1,339 @@ +# ๐Ÿ“‹ Phase 3.11: DDLAuditLogger Raw Query ์ „ํ™˜ ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์š” + +DDLAuditLogger๋Š” **8๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, DDL ์‹คํ–‰ ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +| --------------- | ------------------------------------------------ | +| ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/ddlAuditLogger.ts` | +| ํŒŒ์ผ ํฌ๊ธฐ | 368 ๋ผ์ธ | +| Prisma ํ˜ธ์ถœ | 8๊ฐœ | +| **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **0/8 (0%)** ๐Ÿ”„ **์ง„ํ–‰ ์˜ˆ์ •** | +| ๋ณต์žก๋„ | ์ค‘๊ฐ„ (ํ†ต๊ณ„ ์ฟผ๋ฆฌ, $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) + +--- + +## ๐Ÿ“ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 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 ์กฐ๊ฑด ํฌํ•จ + diff --git a/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md b/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md new file mode 100644 index 00000000..28bee063 --- /dev/null +++ b/PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md @@ -0,0 +1,305 @@ +# ๐Ÿ“‹ Phase 3.12: ExternalCallConfigService Raw Query ์ „ํ™˜ ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์š” + +ExternalCallConfigService๋Š” **8๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, ์™ธ๋ถ€ API ํ˜ธ์ถœ ์„ค์ • ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +| --------------- | --------------------------------------------------------- | +| ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/externalCallConfigService.ts` | +| ํŒŒ์ผ ํฌ๊ธฐ | 581 ๋ผ์ธ | +| Prisma ํ˜ธ์ถœ | 8๊ฐœ | +| **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **0/8 (0%)** ๐Ÿ”„ **์ง„ํ–‰ ์˜ˆ์ •** | +| ๋ณต์žก๋„ | ์ค‘๊ฐ„ (JSON ํ•„๋“œ, ๋ณต์žกํ•œ CRUD) | +| ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸก ์ค‘๊ฐ„ (Phase 3.12) | +| **์ƒํƒœ** | โณ **๋Œ€๊ธฐ ์ค‘** | + +### ๐ŸŽฏ ์ „ํ™˜ ๋ชฉํ‘œ + +- โณ **8๊ฐœ ๋ชจ๋“  Prisma ํ˜ธ์ถœ์„ `db.ts`์˜ `query()`, `queryOne()` ํ•จ์ˆ˜๋กœ ๊ต์ฒด** +- โณ ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • CRUD ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ +- โณ JSON ํ•„๋“œ ์ฒ˜๋ฆฌ (headers, params, auth_config) +- โณ ๋™์  WHERE ์กฐ๊ฑด ์ƒ์„ฑ +- โณ ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™”/๋ณตํ˜ธํ™” ์œ ์ง€ +- โณ TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต +- โณ **Prisma import ์™„์ „ ์ œ๊ฑฐ** + +--- + +## ๐Ÿ” ์˜ˆ์ƒ Prisma ์‚ฌ์šฉ ํŒจํ„ด + +### ์ฃผ์š” ๊ธฐ๋Šฅ (8๊ฐœ ์˜ˆ์ƒ) + +#### 1. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ๋ชฉ๋ก ์กฐํšŒ** +- findMany with filters +- ํŽ˜์ด์ง•, ์ •๋ ฌ +- ๋™์  WHERE ์กฐ๊ฑด (is_active, company_code, search) + +#### 2. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ๋‹จ๊ฑด ์กฐํšŒ** +- findUnique or findFirst +- config_id ๊ธฐ์ค€ + +#### 3. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ์ƒ์„ฑ** +- create +- JSON ํ•„๋“œ ์ฒ˜๋ฆฌ (headers, params, auth_config) +- ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™” + +#### 4. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ์ˆ˜์ •** +- update +- ๋™์  UPDATE ์ฟผ๋ฆฌ +- JSON ํ•„๋“œ ์—…๋ฐ์ดํŠธ + +#### 5. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ์‚ญ์ œ** +- delete or soft delete + +#### 6. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ๋ณต์ œ** +- findUnique + create + +#### 7. **์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • ํ…Œ์ŠคํŠธ** +- findUnique +- ์‹ค์ œ HTTP ํ˜ธ์ถœ + +#### 8. **์™ธ๋ถ€ ํ˜ธ์ถœ ์ด๋ ฅ ์กฐํšŒ** +- findMany with ๊ด€๊ณ„ ์กฐ์ธ +- ํ†ต๊ณ„ ์ฟผ๋ฆฌ + +--- + +## ๐Ÿ’ก ์ „ํ™˜ ์ „๋žต + +### 1๋‹จ๊ณ„: ๊ธฐ๋ณธ CRUD ์ „ํ™˜ (5๊ฐœ) +- getExternalCallConfigs() - ๋ชฉ๋ก ์กฐํšŒ +- getExternalCallConfig() - ๋‹จ๊ฑด ์กฐํšŒ +- createExternalCallConfig() - ์ƒ์„ฑ +- updateExternalCallConfig() - ์ˆ˜์ • +- deleteExternalCallConfig() - ์‚ญ์ œ + +### 2๋‹จ๊ณ„: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ์ „ํ™˜ (3๊ฐœ) +- duplicateExternalCallConfig() - ๋ณต์ œ +- testExternalCallConfig() - ํ…Œ์ŠคํŠธ +- getExternalCallHistory() - ์ด๋ ฅ ์กฐํšŒ + +--- + +## ๐Ÿ’ป ์ „ํ™˜ ์˜ˆ์‹œ + +### ์˜ˆ์‹œ 1: ๋ชฉ๋ก ์กฐํšŒ (๋™์  WHERE + JSON) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +const configs = await prisma.external_call_configs.findMany({ + where: { + company_code: companyCode, + is_active: isActive, + OR: [ + { config_name: { contains: search, mode: "insensitive" } }, + { endpoint_url: { contains: search, mode: "insensitive" } }, + ], + }, + orderBy: { created_at: "desc" }, + skip, + take: limit, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +const conditions: string[] = ["company_code = $1"]; +const params: any[] = [companyCode]; +let paramIndex = 2; + +if (isActive !== undefined) { + conditions.push(`is_active = $${paramIndex++}`); + params.push(isActive); +} + +if (search) { + conditions.push( + `(config_name ILIKE $${paramIndex} OR endpoint_url ILIKE $${paramIndex})` + ); + params.push(`%${search}%`); + paramIndex++; +} + +const configs = await query( + `SELECT * FROM external_call_configs + WHERE ${conditions.join(" AND ")} + ORDER BY created_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, + [...params, limit, skip] +); +``` + +### ์˜ˆ์‹œ 2: JSON ํ•„๋“œ ์ƒ์„ฑ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +const config = await prisma.external_call_configs.create({ + data: { + config_name: data.config_name, + endpoint_url: data.endpoint_url, + http_method: data.http_method, + headers: data.headers, // JSON + params: data.params, // JSON + auth_config: encryptedAuthConfig, // JSON (์•”ํ˜ธํ™”๋จ) + company_code: companyCode, + }, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +const config = await queryOne( + `INSERT INTO external_call_configs + (config_name, endpoint_url, http_method, headers, params, + auth_config, company_code, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) + RETURNING *`, + [ + data.config_name, + data.endpoint_url, + data.http_method, + JSON.stringify(data.headers), + JSON.stringify(data.params), + JSON.stringify(encryptedAuthConfig), + companyCode, + ] +); +``` + +### ์˜ˆ์‹œ 3: ๋™์  UPDATE (JSON ํฌํ•จ) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +const updateData: any = {}; +if (data.headers) updateData.headers = data.headers; +if (data.params) updateData.params = data.params; + +const config = await prisma.external_call_configs.update({ + where: { config_id: configId }, + data: updateData, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +const updateFields: string[] = ["updated_at = NOW()"]; +const values: any[] = []; +let paramIndex = 1; + +if (data.headers !== undefined) { + updateFields.push(`headers = $${paramIndex++}`); + values.push(JSON.stringify(data.headers)); +} + +if (data.params !== undefined) { + updateFields.push(`params = $${paramIndex++}`); + values.push(JSON.stringify(data.params)); +} + +const config = await queryOne( + `UPDATE external_call_configs + SET ${updateFields.join(", ")} + WHERE config_id = $${paramIndex} + RETURNING *`, + [...values, configId] +); +``` + +--- + +## ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. JSON ํ•„๋“œ ์ฒ˜๋ฆฌ +3๊ฐœ์˜ JSON ํ•„๋“œ๊ฐ€ ์žˆ์„ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ: +- `headers` - HTTP ํ—ค๋” +- `params` - ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ +- `auth_config` - ์ธ์ฆ ์„ค์ • (์•”ํ˜ธํ™”๋จ) + +```typescript +// INSERT/UPDATE ์‹œ +JSON.stringify(jsonData) + +// SELECT ํ›„ +const parsedData = typeof row.headers === 'string' + ? JSON.parse(row.headers) + : row.headers; +``` + +### 2. ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™” +auth_config๋Š” ์•”ํ˜ธํ™”๋˜์–ด ์ €์žฅ๋˜๋ฏ€๋กœ, ๊ธฐ์กด ์•”ํ˜ธํ™”/๋ณตํ˜ธํ™” ๋กœ์ง ์œ ์ง€: +```typescript +import { encrypt, decrypt } from "../utils/encryption"; + +// ์ €์žฅ ์‹œ +const encryptedAuthConfig = encrypt(JSON.stringify(authConfig)); + +// ์กฐํšŒ ์‹œ +const decryptedAuthConfig = JSON.parse(decrypt(row.auth_config)); +``` + +### 3. HTTP ๋ฉ”์†Œ๋“œ ๊ฒ€์ฆ +```typescript +const VALID_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]; +if (!VALID_HTTP_METHODS.includes(httpMethod)) { + throw new Error("Invalid HTTP method"); +} +``` + +### 4. URL ๊ฒ€์ฆ +```typescript +try { + new URL(endpointUrl); +} catch { + throw new Error("Invalid endpoint URL"); +} +``` + +--- + +## ๐Ÿ“ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 1๋‹จ๊ณ„: Prisma ํ˜ธ์ถœ ์ „ํ™˜ +- [ ] `getExternalCallConfigs()` - ๋ชฉ๋ก ์กฐํšŒ (findMany + count) +- [ ] `getExternalCallConfig()` - ๋‹จ๊ฑด ์กฐํšŒ (findUnique) +- [ ] `createExternalCallConfig()` - ์ƒ์„ฑ (create) +- [ ] `updateExternalCallConfig()` - ์ˆ˜์ • (update) +- [ ] `deleteExternalCallConfig()` - ์‚ญ์ œ (delete) +- [ ] `duplicateExternalCallConfig()` - ๋ณต์ œ (findUnique + create) +- [ ] `testExternalCallConfig()` - ํ…Œ์ŠคํŠธ (findUnique) +- [ ] `getExternalCallHistory()` - ์ด๋ ฅ ์กฐํšŒ (findMany) + +### 2๋‹จ๊ณ„: ์ฝ”๋“œ ์ •๋ฆฌ +- [ ] import ๋ฌธ ์ˆ˜์ • (`prisma` โ†’ `query, queryOne`) +- [ ] JSON ํ•„๋“œ ์ฒ˜๋ฆฌ ํ™•์ธ +- [ ] ์•”ํ˜ธํ™”/๋ณตํ˜ธํ™” ๋กœ์ง ์œ ์ง€ +- [ ] Prisma import ์™„์ „ ์ œ๊ฑฐ + +### 3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (8๊ฐœ) +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (3๊ฐœ) +- [ ] ์•”ํ˜ธํ™” ํ…Œ์ŠคํŠธ +- [ ] HTTP ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ + +### 4๋‹จ๊ณ„: ๋ฌธ์„œํ™” +- [ ] ์ „ํ™˜ ์™„๋ฃŒ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ +- [ ] API ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ + +--- + +## ๐ŸŽฏ ์˜ˆ์ƒ ๋‚œ์ด๋„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ + +- **๋‚œ์ด๋„**: โญโญโญ (์ค‘๊ฐ„) + - JSON ํ•„๋“œ ์ฒ˜๋ฆฌ + - ์•”ํ˜ธํ™”/๋ณตํ˜ธํ™” ๋กœ์ง + - HTTP ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ + +- **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1~1.5์‹œ๊ฐ„ + +--- + +**์ƒํƒœ**: โณ **๋Œ€๊ธฐ ์ค‘** +**ํŠน์ด์‚ฌํ•ญ**: JSON ํ•„๋“œ, ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™”, HTTP ํ˜ธ์ถœ ํฌํ•จ + diff --git a/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md b/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md new file mode 100644 index 00000000..06676de8 --- /dev/null +++ b/PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md @@ -0,0 +1,283 @@ +# ๐Ÿ“‹ Phase 3.13: EntityJoinService Raw Query ์ „ํ™˜ ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์š” + +EntityJoinService๋Š” **5๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, ์—”ํ‹ฐํ‹ฐ ๊ฐ„ ์กฐ์ธ ๊ด€๊ณ„ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +| --------------- | ------------------------------------------------ | +| ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/entityJoinService.ts` | +| ํŒŒ์ผ ํฌ๊ธฐ | 574 ๋ผ์ธ | +| Prisma ํ˜ธ์ถœ | 5๊ฐœ | +| **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **0/5 (0%)** ๐Ÿ”„ **์ง„ํ–‰ ์˜ˆ์ •** | +| ๋ณต์žก๋„ | ์ค‘๊ฐ„ (์กฐ์ธ ์ฟผ๋ฆฌ, ๊ด€๊ณ„ ์„ค์ •) | +| ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸก ์ค‘๊ฐ„ (Phase 3.13) | +| **์ƒํƒœ** | โณ **๋Œ€๊ธฐ ์ค‘** | + +### ๐ŸŽฏ ์ „ํ™˜ ๋ชฉํ‘œ + +- โณ **5๊ฐœ ๋ชจ๋“  Prisma ํ˜ธ์ถœ์„ `db.ts`์˜ `query()`, `queryOne()` ํ•จ์ˆ˜๋กœ ๊ต์ฒด** +- โณ ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์„ค์ • CRUD ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ +- โณ ๋ณต์žกํ•œ ์กฐ์ธ ์ฟผ๋ฆฌ ์ „ํ™˜ (LEFT JOIN, INNER JOIN) +- โณ ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ +- โณ TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต +- โณ **Prisma import ์™„์ „ ์ œ๊ฑฐ** + +--- + +## ๐Ÿ” ์˜ˆ์ƒ Prisma ์‚ฌ์šฉ ํŒจํ„ด + +### ์ฃผ์š” ๊ธฐ๋Šฅ (5๊ฐœ ์˜ˆ์ƒ) + +#### 1. **์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ๋ชฉ๋ก ์กฐํšŒ** +- findMany with filters +- ๋™์  WHERE ์กฐ๊ฑด +- ํŽ˜์ด์ง•, ์ •๋ ฌ + +#### 2. **์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ๋‹จ๊ฑด ์กฐํšŒ** +- findUnique or findFirst +- join_id ๊ธฐ์ค€ + +#### 3. **์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ƒ์„ฑ** +- create +- ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + +#### 4. **์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ˆ˜์ •** +- update +- ๋™์  UPDATE ์ฟผ๋ฆฌ + +#### 5. **์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์‚ญ์ œ** +- delete + +--- + +## ๐Ÿ’ก ์ „ํ™˜ ์ „๋žต + +### 1๋‹จ๊ณ„: ๊ธฐ๋ณธ CRUD ์ „ํ™˜ (5๊ฐœ) +- getEntityJoins() - ๋ชฉ๋ก ์กฐํšŒ +- getEntityJoin() - ๋‹จ๊ฑด ์กฐํšŒ +- createEntityJoin() - ์ƒ์„ฑ +- updateEntityJoin() - ์ˆ˜์ • +- deleteEntityJoin() - ์‚ญ์ œ + +--- + +## ๐Ÿ’ป ์ „ํ™˜ ์˜ˆ์‹œ + +### ์˜ˆ์‹œ 1: ์กฐ์ธ ์„ค์ • ์กฐํšŒ (LEFT JOIN์œผ๋กœ ํ…Œ์ด๋ธ” ์ •๋ณด ํฌํ•จ) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +const joins = await prisma.entity_joins.findMany({ + where: { + company_code: companyCode, + is_active: true, + }, + include: { + source_table: true, + target_table: true, + }, + orderBy: { created_at: "desc" }, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +const joins = await query( + `SELECT + ej.*, + st.table_name as source_table_name, + st.table_label as source_table_label, + tt.table_name as target_table_name, + tt.table_label as target_table_label + FROM entity_joins ej + LEFT JOIN tables st ON ej.source_table_id = st.table_id + LEFT JOIN tables tt ON ej.target_table_id = tt.table_id + WHERE ej.company_code = $1 AND ej.is_active = $2 + ORDER BY ej.created_at DESC`, + [companyCode, true] +); +``` + +### ์˜ˆ์‹œ 2: ์กฐ์ธ ์ƒ์„ฑ (์œ ํšจ์„ฑ ๊ฒ€์ฆ ํฌํ•จ) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +// ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ +const sourceTable = await prisma.tables.findUnique({ + where: { table_id: sourceTableId }, +}); + +const targetTable = await prisma.tables.findUnique({ + where: { table_id: targetTableId }, +}); + +if (!sourceTable || !targetTable) { + throw new Error("Invalid table references"); +} + +// ์กฐ์ธ ์ƒ์„ฑ +const join = await prisma.entity_joins.create({ + data: { + source_table_id: sourceTableId, + target_table_id: targetTableId, + join_type: joinType, + join_condition: joinCondition, + company_code: companyCode, + }, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +// ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (Promise.all๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰) +const [sourceTable, targetTable] = await Promise.all([ + queryOne( + `SELECT * FROM tables WHERE table_id = $1`, + [sourceTableId] + ), + queryOne( + `SELECT * FROM tables WHERE table_id = $1`, + [targetTableId] + ), +]); + +if (!sourceTable || !targetTable) { + throw new Error("Invalid table references"); +} + +// ์กฐ์ธ ์ƒ์„ฑ +const join = await queryOne( + `INSERT INTO entity_joins + (source_table_id, target_table_id, join_type, join_condition, + company_code, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) + RETURNING *`, + [sourceTableId, targetTableId, joinType, joinCondition, companyCode] +); +``` + +### ์˜ˆ์‹œ 3: ์กฐ์ธ ์ˆ˜์ • + +**๋ณ€๊ฒฝ ์ „**: +```typescript +const join = await prisma.entity_joins.update({ + where: { join_id: joinId }, + data: { + join_type: joinType, + join_condition: joinCondition, + is_active: isActive, + }, +}); +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +const updateFields: string[] = ["updated_at = NOW()"]; +const values: any[] = []; +let paramIndex = 1; + +if (joinType !== undefined) { + updateFields.push(`join_type = $${paramIndex++}`); + values.push(joinType); +} + +if (joinCondition !== undefined) { + updateFields.push(`join_condition = $${paramIndex++}`); + values.push(joinCondition); +} + +if (isActive !== undefined) { + updateFields.push(`is_active = $${paramIndex++}`); + values.push(isActive); +} + +const join = await queryOne( + `UPDATE entity_joins + SET ${updateFields.join(", ")} + WHERE join_id = $${paramIndex} + RETURNING *`, + [...values, joinId] +); +``` + +--- + +## ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. ์กฐ์ธ ํƒ€์ž… ๊ฒ€์ฆ +```typescript +const VALID_JOIN_TYPES = ["INNER", "LEFT", "RIGHT", "FULL"]; +if (!VALID_JOIN_TYPES.includes(joinType)) { + throw new Error("Invalid join type"); +} +``` + +### 2. ์กฐ์ธ ์กฐ๊ฑด ๊ฒ€์ฆ +```typescript +// ์กฐ์ธ ์กฐ๊ฑด์€ SQL ์กฐ๊ฑด์‹ ํ˜•ํƒœ (์˜ˆ: "source.id = target.parent_id") +// SQL ์ธ์ ์…˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๊ฒ€์ฆ ํ•„์š” +const isValidJoinCondition = /^[\w\s.=<>]+$/.test(joinCondition); +if (!isValidJoinCondition) { + throw new Error("Invalid join condition"); +} +``` + +### 3. ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ +```typescript +// ์กฐ์ธ์ด ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆ +async function checkCircularReference( + sourceTableId: number, + targetTableId: number +): Promise { + // ์žฌ๊ท€์ ์œผ๋กœ ์กฐ์ธ ๊ด€๊ณ„ ํ™•์ธ + // ... +} +``` + +### 4. LEFT JOIN์œผ๋กœ ๊ด€๋ จ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ +์กฐ์ธ ์„ค์ • ์กฐํšŒ ์‹œ source/target ํ…Œ์ด๋ธ” ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด LEFT JOIN ์‚ฌ์šฉ + +--- + +## ๐Ÿ“ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 1๋‹จ๊ณ„: Prisma ํ˜ธ์ถœ ์ „ํ™˜ +- [ ] `getEntityJoins()` - ๋ชฉ๋ก ์กฐํšŒ (findMany with include) +- [ ] `getEntityJoin()` - ๋‹จ๊ฑด ์กฐํšŒ (findUnique) +- [ ] `createEntityJoin()` - ์ƒ์„ฑ (create with validation) +- [ ] `updateEntityJoin()` - ์ˆ˜์ • (update) +- [ ] `deleteEntityJoin()` - ์‚ญ์ œ (delete) + +### 2๋‹จ๊ณ„: ์ฝ”๋“œ ์ •๋ฆฌ +- [ ] import ๋ฌธ ์ˆ˜์ • (`prisma` โ†’ `query, queryOne`) +- [ ] ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง ์œ ์ง€ +- [ ] Prisma import ์™„์ „ ์ œ๊ฑฐ + +### 3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (5๊ฐœ) +- [ ] ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ +- [ ] ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ ํ…Œ์ŠคํŠธ +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (2๊ฐœ) + +### 4๋‹จ๊ณ„: ๋ฌธ์„œํ™” +- [ ] ์ „ํ™˜ ์™„๋ฃŒ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ + +--- + +## ๐ŸŽฏ ์˜ˆ์ƒ ๋‚œ์ด๋„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ + +- **๋‚œ์ด๋„**: โญโญโญ (์ค‘๊ฐ„) + - LEFT JOIN ์ฟผ๋ฆฌ + - ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + - ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ + +- **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1์‹œ๊ฐ„ + +--- + +**์ƒํƒœ**: โณ **๋Œ€๊ธฐ ์ค‘** +**ํŠน์ด์‚ฌํ•ญ**: LEFT JOIN, ์กฐ์ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ, ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ ํฌํ•จ + diff --git a/PHASE3.14_AUTH_SERVICE_MIGRATION.md b/PHASE3.14_AUTH_SERVICE_MIGRATION.md new file mode 100644 index 00000000..4180b1d6 --- /dev/null +++ b/PHASE3.14_AUTH_SERVICE_MIGRATION.md @@ -0,0 +1,388 @@ +# ๐Ÿ“‹ Phase 3.14: AuthService Raw Query ์ „ํ™˜ ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์š” + +AuthService๋Š” **5๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด + +| ํ•ญ๋ชฉ | ๋‚ด์šฉ | +| --------------- | ------------------------------------------ | +| ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/authService.ts` | +| ํŒŒ์ผ ํฌ๊ธฐ | 334 ๋ผ์ธ | +| Prisma ํ˜ธ์ถœ | 5๊ฐœ | +| **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **0/5 (0%)** ๐Ÿ”„ **์ง„ํ–‰ ์˜ˆ์ •** | +| ๋ณต์žก๋„ | ๋†’์Œ (๋ณด์•ˆ, ์•”ํ˜ธํ™”, ์„ธ์…˜ ๊ด€๋ฆฌ) | +| ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸก ์ค‘๊ฐ„ (Phase 3.14) | +| **์ƒํƒœ** | โณ **๋Œ€๊ธฐ ์ค‘** | + +### ๐ŸŽฏ ์ „ํ™˜ ๋ชฉํ‘œ + +- โณ **5๊ฐœ ๋ชจ๋“  Prisma ํ˜ธ์ถœ์„ `db.ts`์˜ `query()`, `queryOne()` ํ•จ์ˆ˜๋กœ ๊ต์ฒด** +- โณ ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ธฐ๋Šฅ ์ •์ƒ ๋™์ž‘ +- โณ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”/๊ฒ€์ฆ ์œ ์ง€ +- โณ ์„ธ์…˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์œ ์ง€ +- โณ ๊ถŒํ•œ ๊ฒ€์ฆ ๊ธฐ๋Šฅ ์œ ์ง€ +- โณ TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต +- โณ **Prisma import ์™„์ „ ์ œ๊ฑฐ** + +--- + +## ๐Ÿ” ์˜ˆ์ƒ Prisma ์‚ฌ์šฉ ํŒจํ„ด + +### ์ฃผ์š” ๊ธฐ๋Šฅ (5๊ฐœ ์˜ˆ์ƒ) + +#### 1. **์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ (์ธ์ฆ)** +- findFirst or findUnique +- ์ด๋ฉ”์ผ/์‚ฌ์šฉ์ž๋ช…์œผ๋กœ ์กฐํšŒ +- ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ + +#### 2. **์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ** +- findUnique +- user_id ๊ธฐ์ค€ +- ๊ถŒํ•œ ์ •๋ณด ํฌํ•จ + +#### 3. **์‚ฌ์šฉ์ž ์ƒ์„ฑ (ํšŒ์›๊ฐ€์ž…)** +- create +- ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” +- ์ค‘๋ณต ๊ฒ€์‚ฌ + +#### 4. **๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ** +- update +- ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ +- ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” + +#### 5. **์„ธ์…˜ ๊ด€๋ฆฌ** +- create, update, delete +- ์„ธ์…˜ ํ† ํฐ ์ €์žฅ/์กฐํšŒ + +--- + +## ๐Ÿ’ก ์ „ํ™˜ ์ „๋žต + +### 1๋‹จ๊ณ„: ์ธ์ฆ ๊ด€๋ จ ์ „ํ™˜ (2๊ฐœ) +- login() - ์‚ฌ์šฉ์ž ์กฐํšŒ + ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ +- getUserInfo() - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ + +### 2๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ ์ „ํ™˜ (2๊ฐœ) +- createUser() - ์‚ฌ์šฉ์ž ์ƒ์„ฑ +- changePassword() - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + +### 3๋‹จ๊ณ„: ์„ธ์…˜ ๊ด€๋ฆฌ ์ „ํ™˜ (1๊ฐœ) +- manageSession() - ์„ธ์…˜ CRUD + +--- + +## ๐Ÿ’ป ์ „ํ™˜ ์˜ˆ์‹œ + +### ์˜ˆ์‹œ 1: ๋กœ๊ทธ์ธ (๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +async login(username: string, password: string) { + const user = await prisma.users.findFirst({ + where: { + OR: [ + { username: username }, + { email: username }, + ], + is_active: true, + }, + }); + + if (!user) { + throw new Error("User not found"); + } + + const isPasswordValid = await bcrypt.compare(password, user.password_hash); + + if (!isPasswordValid) { + throw new Error("Invalid password"); + } + + return user; +} +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +async login(username: string, password: string) { + const user = await queryOne( + `SELECT * FROM users + WHERE (username = $1 OR email = $1) + AND is_active = $2`, + [username, true] + ); + + if (!user) { + throw new Error("User not found"); + } + + const isPasswordValid = await bcrypt.compare(password, user.password_hash); + + if (!isPasswordValid) { + throw new Error("Invalid password"); + } + + return user; +} +``` + +### ์˜ˆ์‹œ 2: ์‚ฌ์šฉ์ž ์ƒ์„ฑ (๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”) + +**๋ณ€๊ฒฝ ์ „**: +```typescript +async createUser(userData: CreateUserDto) { + // ์ค‘๋ณต ๊ฒ€์‚ฌ + const existing = await prisma.users.findFirst({ + where: { + OR: [ + { username: userData.username }, + { email: userData.email }, + ], + }, + }); + + if (existing) { + throw new Error("User already exists"); + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” + const passwordHash = await bcrypt.hash(userData.password, 10); + + // ์‚ฌ์šฉ์ž ์ƒ์„ฑ + const user = await prisma.users.create({ + data: { + username: userData.username, + email: userData.email, + password_hash: passwordHash, + company_code: userData.company_code, + }, + }); + + return user; +} +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +async createUser(userData: CreateUserDto) { + // ์ค‘๋ณต ๊ฒ€์‚ฌ + const existing = await queryOne( + `SELECT * FROM users + WHERE username = $1 OR email = $2`, + [userData.username, userData.email] + ); + + if (existing) { + throw new Error("User already exists"); + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” + const passwordHash = await bcrypt.hash(userData.password, 10); + + // ์‚ฌ์šฉ์ž ์ƒ์„ฑ + const user = await queryOne( + `INSERT INTO users + (username, email, password_hash, company_code, created_at, updated_at) + VALUES ($1, $2, $3, $4, NOW(), NOW()) + RETURNING *`, + [userData.username, userData.email, passwordHash, userData.company_code] + ); + + return user; +} +``` + +### ์˜ˆ์‹œ 3: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + +**๋ณ€๊ฒฝ ์ „**: +```typescript +async changePassword( + userId: number, + oldPassword: string, + newPassword: string +) { + const user = await prisma.users.findUnique({ + where: { user_id: userId }, + }); + + if (!user) { + throw new Error("User not found"); + } + + const isOldPasswordValid = await bcrypt.compare( + oldPassword, + user.password_hash + ); + + if (!isOldPasswordValid) { + throw new Error("Invalid old password"); + } + + const newPasswordHash = await bcrypt.hash(newPassword, 10); + + await prisma.users.update({ + where: { user_id: userId }, + data: { password_hash: newPasswordHash }, + }); +} +``` + +**๋ณ€๊ฒฝ ํ›„**: +```typescript +async changePassword( + userId: number, + oldPassword: string, + newPassword: string +) { + const user = await queryOne( + `SELECT * FROM users WHERE user_id = $1`, + [userId] + ); + + if (!user) { + throw new Error("User not found"); + } + + const isOldPasswordValid = await bcrypt.compare( + oldPassword, + user.password_hash + ); + + if (!isOldPasswordValid) { + throw new Error("Invalid old password"); + } + + const newPasswordHash = await bcrypt.hash(newPassword, 10); + + await query( + `UPDATE users + SET password_hash = $1, updated_at = NOW() + WHERE user_id = $2`, + [newPasswordHash, userId] + ); +} +``` + +--- + +## ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด์•ˆ +```typescript +import bcrypt from "bcrypt"; + +// ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ (ํšŒ์›๊ฐ€์ž…, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) +const SALT_ROUNDS = 10; +const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + +// ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (๋กœ๊ทธ์ธ) +const isValid = await bcrypt.compare(plainPassword, passwordHash); +``` + +### 2. SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ +```typescript +// โŒ ์œ„ํ—˜: ์ง์ ‘ ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ +const sql = `SELECT * FROM users WHERE username = '${username}'`; + +// โœ… ์•ˆ์ „: ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ +const user = await queryOne(`SELECT * FROM users WHERE username = $1`, [username]); +``` + +### 3. ์„ธ์…˜ ํ† ํฐ ๊ด€๋ฆฌ +```typescript +import crypto from "crypto"; + +// ์„ธ์…˜ ํ† ํฐ ์ƒ์„ฑ +const sessionToken = crypto.randomBytes(32).toString("hex"); + +// ์„ธ์…˜ ์ €์žฅ +await query( + `INSERT INTO user_sessions (user_id, session_token, expires_at) + VALUES ($1, $2, NOW() + INTERVAL '1 day')`, + [userId, sessionToken] +); +``` + +### 4. ๊ถŒํ•œ ๊ฒ€์ฆ +```typescript +async checkPermission(userId: number, permission: string): Promise { + const result = await queryOne<{ has_permission: boolean }>( + `SELECT EXISTS ( + SELECT 1 FROM user_permissions up + JOIN permissions p ON up.permission_id = p.permission_id + WHERE up.user_id = $1 AND p.permission_name = $2 + ) as has_permission`, + [userId, permission] + ); + + return result?.has_permission || false; +} +``` + +--- + +## ๐Ÿ“ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 1๋‹จ๊ณ„: Prisma ํ˜ธ์ถœ ์ „ํ™˜ +- [ ] `login()` - ์‚ฌ์šฉ์ž ์กฐํšŒ + ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (findFirst) +- [ ] `getUserInfo()` - ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ (findUnique) +- [ ] `createUser()` - ์‚ฌ์šฉ์ž ์ƒ์„ฑ (create with ์ค‘๋ณต ๊ฒ€์‚ฌ) +- [ ] `changePassword()` - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ (findUnique + update) +- [ ] `manageSession()` - ์„ธ์…˜ ๊ด€๋ฆฌ (create/update/delete) + +### 2๋‹จ๊ณ„: ๋ณด์•ˆ ๊ฒ€์ฆ +- [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ ๋กœ์ง ์œ ์ง€ (bcrypt) +- [ ] SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ํ™•์ธ +- [ ] ์„ธ์…˜ ํ† ํฐ ๋ณด์•ˆ ํ™•์ธ +- [ ] ์ค‘๋ณต ๊ณ„์ • ๋ฐฉ์ง€ ํ™•์ธ + +### 3๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ +- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (5๊ฐœ) + - [ ] ๋กœ๊ทธ์ธ ์„ฑ๊ณต/์‹คํŒจ ํ…Œ์ŠคํŠธ + - [ ] ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ…Œ์ŠคํŠธ + - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํ…Œ์ŠคํŠธ + - [ ] ์„ธ์…˜ ๊ด€๋ฆฌ ํ…Œ์ŠคํŠธ + - [ ] ๊ถŒํ•œ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ +- [ ] ๋ณด์•ˆ ํ…Œ์ŠคํŠธ + - [ ] SQL ์ธ์ ์…˜ ํ…Œ์ŠคํŠธ + - [ ] ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ•๋„ ํ…Œ์ŠคํŠธ + - [ ] ์„ธ์…˜ ํƒˆ์ทจ ๋ฐฉ์ง€ ํ…Œ์ŠคํŠธ +- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (2๊ฐœ) + +### 4๋‹จ๊ณ„: ๋ฌธ์„œํ™” +- [ ] ์ „ํ™˜ ์™„๋ฃŒ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ +- [ ] ๋ณด์•ˆ ๊ฐ€์ด๋“œ ์—…๋ฐ์ดํŠธ + +--- + +## ๐ŸŽฏ ์˜ˆ์ƒ ๋‚œ์ด๋„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ + +- **๋‚œ์ด๋„**: โญโญโญโญ (๋†’์Œ) + - ๋ณด์•ˆ ํฌ๋ฆฌํ‹ฐ์ปฌ (๋น„๋ฐ€๋ฒˆํ˜ธ, ์„ธ์…˜) + - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ํ•„์ˆ˜ + - ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ ํ•„์š” + +- **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1.5~2์‹œ๊ฐ„ + - Prisma ํ˜ธ์ถœ ์ „ํ™˜: 40๋ถ„ + - ๋ณด์•ˆ ๊ฒ€์ฆ: 40๋ถ„ + - ํ…Œ์ŠคํŠธ: 40๋ถ„ + +--- + +## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ + +### ๋ณด์•ˆ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ +1. โœ… ๋ชจ๋“  ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ +2. โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์ ˆ๋Œ€ ํ‰๋ฌธ ์ €์žฅ ๊ธˆ์ง€ (bcrypt ์‚ฌ์šฉ) +3. โœ… ์„ธ์…˜ ํ† ํฐ์€ ์ถฉ๋ถ„ํžˆ ๊ธธ๊ณ  ๋žœ๋คํ•ด์•ผ ํ•จ +4. โœ… ๋น„๋ฐ€๋ฒˆํ˜ธ ์‹คํŒจ ์‹œ ๊ตฌ์ฒด์  ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๊ธˆ์ง€ ("User not found" vs "Invalid credentials") +5. โœ… ๋กœ๊ทธ์ธ ์‹คํŒจ ํšŸ์ˆ˜ ์ œํ•œ (Brute Force ๋ฐฉ์ง€) + +--- + +**์ƒํƒœ**: โณ **๋Œ€๊ธฐ ์ค‘** +**ํŠน์ด์‚ฌํ•ญ**: ๋ณด์•ˆ ํฌ๋ฆฌํ‹ฐ์ปฌ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”, ์„ธ์…˜ ๊ด€๋ฆฌ ํฌํ•จ +**โš ๏ธ ์ฃผ์˜**: ์ด ์„œ๋น„์Šค๋Š” ๋ณด์•ˆ์— ๋งค์šฐ ์ค‘์š”ํ•˜๋ฏ€๋กœ ์‹ ์ค‘ํ•œ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜! + diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 645420ae..7d0e3176 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -135,16 +135,16 @@ backend-node/ (๋ฃจํŠธ) #### ๐ŸŸก **์ค‘๊ฐ„ (๋‹จ์ˆœ CRUD) - 3์ˆœ์œ„** -- `ddlAuditLogger.ts` (8๊ฐœ) - DDL ๊ฐ์‚ฌ ๋กœ๊ทธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ -- `externalCallConfigService.ts` (8๊ฐœ) - ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `ddlAuditLogger.ts` (8๊ฐœ) - DDL ๊ฐ์‚ฌ ๋กœ๊ทธ - [๊ณ„ํš์„œ](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md) +- `externalCallConfigService.ts` (8๊ฐœ) - ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • - [๊ณ„ํš์„œ](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md) - `batchExternalDbService.ts` (8๊ฐœ) - ๋ฐฐ์น˜ ์™ธ๋ถ€DB โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ - `batchExecutionLogService.ts` (7๊ฐœ) - ๋ฐฐ์น˜ ์‹คํ–‰ ๋กœ๊ทธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ - `enhancedDynamicFormService.ts` (6๊ฐœ) - ํ™•์žฅ ๋™์  ํผ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ -- `ddlExecutionService.ts` (6๊ฐœ) - DDL ์‹คํ–‰ -- `entityJoinService.ts` (5๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `ddlExecutionService.ts` (6๊ฐœ) - DDL ์‹คํ–‰ (์ด๋ฏธ ์ „ํ™˜ ์™„๋ฃŒ?) +- `entityJoinService.ts` (5๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ - [๊ณ„ํš์„œ](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md) - `dataMappingService.ts` (5๊ฐœ) - ๋ฐ์ดํ„ฐ ๋งคํ•‘ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ - `batchManagementService.ts` (5๊ฐœ) - ๋ฐฐ์น˜ ๊ด€๋ฆฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ -- `authService.ts` (5๊ฐœ) - ์‚ฌ์šฉ์ž ์ธ์ฆ +- `authService.ts` (5๊ฐœ) - ์‚ฌ์šฉ์ž ์ธ์ฆ - [๊ณ„ํš์„œ](PHASE3.14_AUTH_SERVICE_MIGRATION.md) - `batchSchedulerService.ts` (4๊ฐœ) - ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ - `dataService.ts` (4๊ฐœ) - ๋ฐ์ดํ„ฐ ์„œ๋น„์Šค โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ - `adminService.ts` (3๊ฐœ) - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด