386 lines
9.8 KiB
Markdown
386 lines
9.8 KiB
Markdown
|
|
# Phase 4.1: AdminController Raw Query 전환 계획
|
||
|
|
|
||
|
|
## 📋 개요
|
||
|
|
|
||
|
|
관리자 컨트롤러의 Prisma 호출을 Raw Query로 전환합니다.
|
||
|
|
사용자, 회사, 부서, 메뉴 관리 등 핵심 관리 기능을 포함합니다.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 📊 기본 정보
|
||
|
|
|
||
|
|
| 항목 | 내용 |
|
||
|
|
| --------------- | --------------------------------------------- |
|
||
|
|
| 파일 위치 | `backend-node/src/controllers/adminController.ts` |
|
||
|
|
| 파일 크기 | 2,571 라인 |
|
||
|
|
| Prisma 호출 | 28개 |
|
||
|
|
| **현재 진행률** | **0/28 (0%)** 🔄 **진행 예정** |
|
||
|
|
| 복잡도 | 중간 (다양한 CRUD 패턴) |
|
||
|
|
| 우선순위 | 🔴 높음 (Phase 4.1) |
|
||
|
|
| **상태** | ⏳ **대기 중** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🔍 Prisma 호출 분석
|
||
|
|
|
||
|
|
### 사용자 관리 (13개)
|
||
|
|
|
||
|
|
#### 1. getUserList (라인 312-317)
|
||
|
|
```typescript
|
||
|
|
const totalCount = await prisma.user_info.count({ where });
|
||
|
|
const users = await prisma.user_info.findMany({ where, skip, take, orderBy });
|
||
|
|
```
|
||
|
|
- **전환**: count → `queryOne`, findMany → `query`
|
||
|
|
- **복잡도**: 중간 (동적 WHERE, 페이징)
|
||
|
|
|
||
|
|
#### 2. getUserInfo (라인 419)
|
||
|
|
```typescript
|
||
|
|
const userInfo = await prisma.user_info.findFirst({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findFirst → `queryOne`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 3. updateUserStatus (라인 498)
|
||
|
|
```typescript
|
||
|
|
await prisma.user_info.update({ where, data });
|
||
|
|
```
|
||
|
|
- **전환**: update → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 4. deleteUserByAdmin (라인 2387)
|
||
|
|
```typescript
|
||
|
|
await prisma.user_info.update({ where, data: { is_active: 'N' } });
|
||
|
|
```
|
||
|
|
- **전환**: update (soft delete) → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 5. getMyProfile (라인 1468, 1488, 2479)
|
||
|
|
```typescript
|
||
|
|
const user = await prisma.user_info.findUnique({ where });
|
||
|
|
const dept = await prisma.dept_info.findUnique({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findUnique → `queryOne`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 6. updateMyProfile (라인 1864, 2527)
|
||
|
|
```typescript
|
||
|
|
const updateResult = await prisma.user_info.update({ where, data });
|
||
|
|
```
|
||
|
|
- **전환**: update → `queryOne` with RETURNING
|
||
|
|
- **복잡도**: 중간 (동적 UPDATE)
|
||
|
|
|
||
|
|
#### 7. createOrUpdateUser (라인 1929, 1975)
|
||
|
|
```typescript
|
||
|
|
const savedUser = await prisma.user_info.upsert({ where, update, create });
|
||
|
|
const userCount = await prisma.user_info.count({ where });
|
||
|
|
```
|
||
|
|
- **전환**: upsert → `INSERT ... ON CONFLICT`, count → `queryOne`
|
||
|
|
- **복잡도**: 높음
|
||
|
|
|
||
|
|
#### 8. 기타 findUnique (라인 1596, 1832, 2393)
|
||
|
|
```typescript
|
||
|
|
const existingUser = await prisma.user_info.findUnique({ where });
|
||
|
|
const currentUser = await prisma.user_info.findUnique({ where });
|
||
|
|
const updatedUser = await prisma.user_info.findUnique({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findUnique → `queryOne`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
### 회사 관리 (7개)
|
||
|
|
|
||
|
|
#### 9. getCompanyList (라인 550, 1276)
|
||
|
|
```typescript
|
||
|
|
const companies = await prisma.company_mng.findMany({ orderBy });
|
||
|
|
```
|
||
|
|
- **전환**: findMany → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 10. createCompany (라인 2035)
|
||
|
|
```typescript
|
||
|
|
const existingCompany = await prisma.company_mng.findFirst({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findFirst (중복 체크) → `queryOne`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 11. updateCompany (라인 2172, 2192)
|
||
|
|
```typescript
|
||
|
|
const duplicateCompany = await prisma.company_mng.findFirst({ where });
|
||
|
|
const updatedCompany = await prisma.company_mng.update({ where, data });
|
||
|
|
```
|
||
|
|
- **전환**: findFirst → `queryOne`, update → `queryOne`
|
||
|
|
- **복잡도**: 중간
|
||
|
|
|
||
|
|
#### 12. deleteCompany (라인 2261, 2281)
|
||
|
|
```typescript
|
||
|
|
const existingCompany = await prisma.company_mng.findUnique({ where });
|
||
|
|
await prisma.company_mng.delete({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findUnique → `queryOne`, delete → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
### 부서 관리 (2개)
|
||
|
|
|
||
|
|
#### 13. getDepartmentList (라인 1348)
|
||
|
|
```typescript
|
||
|
|
const departments = await prisma.dept_info.findMany({ where, orderBy });
|
||
|
|
```
|
||
|
|
- **전환**: findMany → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
#### 14. getDeptInfo (라인 1488)
|
||
|
|
```typescript
|
||
|
|
const dept = await prisma.dept_info.findUnique({ where });
|
||
|
|
```
|
||
|
|
- **전환**: findUnique → `queryOne`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
### 메뉴 관리 (3개)
|
||
|
|
|
||
|
|
#### 15. createMenu (라인 1021)
|
||
|
|
```typescript
|
||
|
|
const savedMenu = await prisma.menu_info.create({ data });
|
||
|
|
```
|
||
|
|
- **전환**: create → `queryOne` with INSERT RETURNING
|
||
|
|
- **복잡도**: 중간
|
||
|
|
|
||
|
|
#### 16. updateMenu (라인 1087)
|
||
|
|
```typescript
|
||
|
|
const updatedMenu = await prisma.menu_info.update({ where, data });
|
||
|
|
```
|
||
|
|
- **전환**: update → `queryOne` with UPDATE RETURNING
|
||
|
|
- **복잡도**: 중간
|
||
|
|
|
||
|
|
#### 17. deleteMenu (라인 1149, 1211)
|
||
|
|
```typescript
|
||
|
|
const deletedMenu = await prisma.menu_info.delete({ where });
|
||
|
|
// 재귀 삭제
|
||
|
|
const deletedMenu = await prisma.menu_info.delete({ where });
|
||
|
|
```
|
||
|
|
- **전환**: delete → `query`
|
||
|
|
- **복잡도**: 중간 (재귀 삭제 로직)
|
||
|
|
|
||
|
|
### 다국어 (1개)
|
||
|
|
|
||
|
|
#### 18. getMultiLangKeys (라인 665)
|
||
|
|
```typescript
|
||
|
|
const result = await prisma.multi_lang_key_master.findMany({ where, orderBy });
|
||
|
|
```
|
||
|
|
- **전환**: findMany → `query`
|
||
|
|
- **복잡도**: 낮음
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📝 전환 전략
|
||
|
|
|
||
|
|
### 1단계: Import 변경
|
||
|
|
```typescript
|
||
|
|
// 제거
|
||
|
|
import { PrismaClient } from "@prisma/client";
|
||
|
|
const prisma = new PrismaClient();
|
||
|
|
|
||
|
|
// 추가
|
||
|
|
import { query, queryOne } from "../database/db";
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2단계: 단순 조회 전환
|
||
|
|
- findMany → `query<T>`
|
||
|
|
- findUnique/findFirst → `queryOne<T>`
|
||
|
|
|
||
|
|
### 3단계: 동적 WHERE 처리
|
||
|
|
```typescript
|
||
|
|
const whereConditions: string[] = [];
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (companyCode) {
|
||
|
|
whereConditions.push(`company_code = $${paramIndex++}`);
|
||
|
|
params.push(companyCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = whereConditions.length > 0
|
||
|
|
? `WHERE ${whereConditions.join(' AND ')}`
|
||
|
|
: '';
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4단계: 복잡한 로직 전환
|
||
|
|
- count → `SELECT COUNT(*) as count`
|
||
|
|
- upsert → `INSERT ... ON CONFLICT DO UPDATE`
|
||
|
|
- 동적 UPDATE → 조건부 SET 절 생성
|
||
|
|
|
||
|
|
### 5단계: 테스트 및 검증
|
||
|
|
- 각 함수별 동작 확인
|
||
|
|
- 에러 처리 확인
|
||
|
|
- 타입 안전성 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 🎯 주요 변경 예시
|
||
|
|
|
||
|
|
### getUserList (count + findMany)
|
||
|
|
```typescript
|
||
|
|
// Before
|
||
|
|
const totalCount = await prisma.user_info.count({ where });
|
||
|
|
const users = await prisma.user_info.findMany({
|
||
|
|
where,
|
||
|
|
skip,
|
||
|
|
take,
|
||
|
|
orderBy
|
||
|
|
});
|
||
|
|
|
||
|
|
// After
|
||
|
|
const whereConditions: string[] = [];
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
// 동적 WHERE 구성
|
||
|
|
if (where.company_code) {
|
||
|
|
whereConditions.push(`company_code = $${paramIndex++}`);
|
||
|
|
params.push(where.company_code);
|
||
|
|
}
|
||
|
|
if (where.user_name) {
|
||
|
|
whereConditions.push(`user_name ILIKE $${paramIndex++}`);
|
||
|
|
params.push(`%${where.user_name}%`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const whereClause = whereConditions.length > 0
|
||
|
|
? `WHERE ${whereConditions.join(' AND ')}`
|
||
|
|
: '';
|
||
|
|
|
||
|
|
// Count
|
||
|
|
const countResult = await queryOne<{ count: number }>(
|
||
|
|
`SELECT COUNT(*) as count FROM user_info ${whereClause}`,
|
||
|
|
params
|
||
|
|
);
|
||
|
|
const totalCount = parseInt(countResult?.count?.toString() || '0', 10);
|
||
|
|
|
||
|
|
// 데이터 조회
|
||
|
|
const usersQuery = `
|
||
|
|
SELECT * FROM user_info
|
||
|
|
${whereClause}
|
||
|
|
ORDER BY created_date DESC
|
||
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||
|
|
`;
|
||
|
|
params.push(take, skip);
|
||
|
|
|
||
|
|
const users = await query<UserInfo>(usersQuery, params);
|
||
|
|
```
|
||
|
|
|
||
|
|
### createOrUpdateUser (upsert)
|
||
|
|
```typescript
|
||
|
|
// Before
|
||
|
|
const savedUser = await prisma.user_info.upsert({
|
||
|
|
where: { user_id: userId },
|
||
|
|
update: updateData,
|
||
|
|
create: createData
|
||
|
|
});
|
||
|
|
|
||
|
|
// After
|
||
|
|
const savedUser = await queryOne<UserInfo>(
|
||
|
|
`INSERT INTO user_info (user_id, user_name, email, ...)
|
||
|
|
VALUES ($1, $2, $3, ...)
|
||
|
|
ON CONFLICT (user_id)
|
||
|
|
DO UPDATE SET
|
||
|
|
user_name = EXCLUDED.user_name,
|
||
|
|
email = EXCLUDED.email,
|
||
|
|
...
|
||
|
|
RETURNING *`,
|
||
|
|
[userId, userName, email, ...]
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### updateMyProfile (동적 UPDATE)
|
||
|
|
```typescript
|
||
|
|
// Before
|
||
|
|
const updateResult = await prisma.user_info.update({
|
||
|
|
where: { user_id: userId },
|
||
|
|
data: updateData
|
||
|
|
});
|
||
|
|
|
||
|
|
// After
|
||
|
|
const updates: string[] = [];
|
||
|
|
const params: any[] = [];
|
||
|
|
let paramIndex = 1;
|
||
|
|
|
||
|
|
if (updateData.user_name !== undefined) {
|
||
|
|
updates.push(`user_name = $${paramIndex++}`);
|
||
|
|
params.push(updateData.user_name);
|
||
|
|
}
|
||
|
|
if (updateData.email !== undefined) {
|
||
|
|
updates.push(`email = $${paramIndex++}`);
|
||
|
|
params.push(updateData.email);
|
||
|
|
}
|
||
|
|
// ... 다른 필드들
|
||
|
|
|
||
|
|
params.push(userId);
|
||
|
|
|
||
|
|
const updateResult = await queryOne<UserInfo>(
|
||
|
|
`UPDATE user_info
|
||
|
|
SET ${updates.join(', ')}, updated_date = NOW()
|
||
|
|
WHERE user_id = $${paramIndex}
|
||
|
|
RETURNING *`,
|
||
|
|
params
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ✅ 체크리스트
|
||
|
|
|
||
|
|
### 기본 설정
|
||
|
|
- [ ] Prisma import 제거
|
||
|
|
- [ ] query, queryOne import 추가
|
||
|
|
- [ ] 타입 import 확인
|
||
|
|
|
||
|
|
### 사용자 관리
|
||
|
|
- [ ] getUserList (count + findMany)
|
||
|
|
- [ ] getUserInfo (findFirst)
|
||
|
|
- [ ] updateUserStatus (update)
|
||
|
|
- [ ] deleteUserByAdmin (soft delete)
|
||
|
|
- [ ] getMyProfile (findUnique x3)
|
||
|
|
- [ ] updateMyProfile (update x2)
|
||
|
|
- [ ] createOrUpdateUser (upsert + count)
|
||
|
|
- [ ] 기타 findUnique (x3)
|
||
|
|
|
||
|
|
### 회사 관리
|
||
|
|
- [ ] getCompanyList (findMany x2)
|
||
|
|
- [ ] createCompany (findFirst 중복체크)
|
||
|
|
- [ ] updateCompany (findFirst + update)
|
||
|
|
- [ ] deleteCompany (findUnique + delete)
|
||
|
|
|
||
|
|
### 부서 관리
|
||
|
|
- [ ] getDepartmentList (findMany)
|
||
|
|
- [ ] getDeptInfo (findUnique)
|
||
|
|
|
||
|
|
### 메뉴 관리
|
||
|
|
- [ ] createMenu (create)
|
||
|
|
- [ ] updateMenu (update)
|
||
|
|
- [ ] deleteMenu (delete x2, 재귀)
|
||
|
|
|
||
|
|
### 다국어
|
||
|
|
- [ ] getMultiLangKeys (findMany)
|
||
|
|
|
||
|
|
### 검증
|
||
|
|
- [ ] TypeScript 컴파일 확인
|
||
|
|
- [ ] Linter 오류 확인
|
||
|
|
- [ ] 기능 테스트
|
||
|
|
- [ ] 에러 처리 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📌 참고사항
|
||
|
|
|
||
|
|
### 동적 쿼리 생성 패턴
|
||
|
|
모든 동적 WHERE/UPDATE는 다음 패턴을 따릅니다:
|
||
|
|
1. 조건/필드 배열 생성
|
||
|
|
2. 파라미터 배열 생성
|
||
|
|
3. 파라미터 인덱스 관리
|
||
|
|
4. SQL 문자열 조합
|
||
|
|
5. query/queryOne 실행
|
||
|
|
|
||
|
|
### 에러 처리
|
||
|
|
기존 try-catch 구조를 유지하며, 데이터베이스 에러를 적절히 변환합니다.
|
||
|
|
|
||
|
|
### 트랜잭션
|
||
|
|
복잡한 로직은 Service Layer로 이동을 고려합니다.
|
||
|
|
|