From ce37626e495dc08eca6408a339608201c47c61ed Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 11:48:55 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=203.11~3.14=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4개 주요 서비스에 대한 상세 전환 계획서 작성: 1. **Phase 3.11: DDLAuditLogger** (8개 호출) - DDL 실행 감사 로그 관리 - 통계 쿼리 (GROUP BY, CASE WHEN, AVG) - 동적 WHERE 조건 - JSON 필드 처리 - 날짜/시간 함수 2. **Phase 3.12: ExternalCallConfigService** (8개 호출) - 외부 API 호출 설정 관리 - JSON 필드 (headers, params, auth_config) - 민감 정보 암호화/복호화 - 동적 CRUD 쿼리 3. **Phase 3.13: EntityJoinService** (5개 호출) - 엔티티 간 조인 관계 관리 - LEFT JOIN 쿼리 - 조인 유효성 검증 - 순환 참조 방지 4. **Phase 3.14: AuthService** (5개 호출) - 사용자 인증 및 권한 관리 - 비밀번호 암호화/검증 (bcrypt) - 세션 토큰 관리 - 보안 크리티컬 - SQL 인젝션 방지 각 계획서 포함 내용: - 파일 정보 및 복잡도 - Prisma 사용 현황 분석 - 전환 전략 (단계별) - 상세 전환 예시 (Before/After) - 기술적 고려사항 - 전환 체크리스트 - 예상 난이도 및 소요 시간 - 보안/성능 주의사항 메인 문서에 계획서 링크 추가 --- PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md | 339 +++++++++++++++ ..._EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md | 305 ++++++++++++++ PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md | 283 +++++++++++++ PHASE3.14_AUTH_SERVICE_MIGRATION.md | 388 ++++++++++++++++++ PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 10 +- 5 files changed, 1320 insertions(+), 5 deletions(-) create mode 100644 PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md create mode 100644 PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md create mode 100644 PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md create mode 100644 PHASE3.14_AUTH_SERVICE_MIGRATION.md 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개) - 관리자 메뉴