docs: Phase 3.11~3.14 상세 마이그레이션 계획서 작성

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)
- 기술적 고려사항
- 전환 체크리스트
- 예상 난이도 및 소요 시간
- 보안/성능 주의사항

메인 문서에 계획서 링크 추가
This commit is contained in:
kjs 2025-10-01 11:48:55 +09:00
parent 134d24579c
commit ce37626e49
5 changed files with 1320 additions and 5 deletions

View File

@ -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<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)
---
## 📝 전환 체크리스트
### 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 조건 포함

View File

@ -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<any>(
`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<any>(
`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<any>(
`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 호출 포함

View File

@ -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<any>(
`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<any>(
`SELECT * FROM tables WHERE table_id = $1`,
[sourceTableId]
),
queryOne<any>(
`SELECT * FROM tables WHERE table_id = $1`,
[targetTableId]
),
]);
if (!sourceTable || !targetTable) {
throw new Error("Invalid table references");
}
// 조인 생성
const join = await queryOne<any>(
`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<any>(
`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<boolean> {
// 재귀적으로 조인 관계 확인
// ...
}
```
### 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, 조인 유효성 검증, 순환 참조 방지 포함

View File

@ -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<any>(
`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<any>(
`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<any>(
`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<any>(
`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<boolean> {
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 방지)
---
**상태**: ⏳ **대기 중**
**특이사항**: 보안 크리티컬, 비밀번호 암호화, 세션 관리 포함
**⚠️ 주의**: 이 서비스는 보안에 매우 중요하므로 신중한 테스트 필수!

View File

@ -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개) - 관리자 메뉴