409 lines
10 KiB
Markdown
409 lines
10 KiB
Markdown
# 📋 Phase 3.9: TemplateStandardService Raw Query 전환 계획
|
|
|
|
## 📋 개요
|
|
|
|
TemplateStandardService는 **6개의 Prisma 호출**이 있으며, 템플릿 표준 관리를 담당하는 서비스입니다.
|
|
|
|
### 📊 기본 정보
|
|
|
|
| 항목 | 내용 |
|
|
| --------------- | ------------------------------------------------------ |
|
|
| 파일 위치 | `backend-node/src/services/templateStandardService.ts` |
|
|
| 파일 크기 | 395 라인 |
|
|
| Prisma 호출 | 6개 |
|
|
| **현재 진행률** | **7/7 (100%)** ✅ **전환 완료** |
|
|
| 복잡도 | 낮음 (기본 CRUD + DISTINCT) |
|
|
| 우선순위 | 🟢 낮음 (Phase 3.9) |
|
|
| **상태** | ✅ **완료** |
|
|
|
|
### 🎯 전환 목표
|
|
|
|
- ✅ **7개 모든 Prisma 호출을 `db.ts`의 `query()`, `queryOne()` 함수로 교체**
|
|
- ✅ 템플릿 CRUD 기능 정상 동작
|
|
- ✅ DISTINCT 쿼리 전환
|
|
- ✅ Promise.all 병렬 쿼리 (목록 + 개수)
|
|
- ✅ 동적 UPDATE 쿼리 (11개 필드)
|
|
- ✅ TypeScript 컴파일 성공
|
|
- ✅ **Prisma import 완전 제거**
|
|
|
|
---
|
|
|
|
## 🔍 Prisma 사용 현황 분석
|
|
|
|
### 주요 Prisma 호출 (6개)
|
|
|
|
#### 1. **getTemplateByCode()** - 템플릿 단건 조회
|
|
|
|
```typescript
|
|
// Line 76
|
|
return await prisma.template_standards.findUnique({
|
|
where: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
});
|
|
```
|
|
|
|
#### 2. **createTemplate()** - 템플릿 생성
|
|
|
|
```typescript
|
|
// Line 86
|
|
const existing = await prisma.template_standards.findUnique({
|
|
where: {
|
|
template_code: data.template_code,
|
|
company_code: data.company_code,
|
|
},
|
|
});
|
|
|
|
// Line 96
|
|
return await prisma.template_standards.create({
|
|
data: {
|
|
...data,
|
|
created_date: new Date(),
|
|
updated_date: new Date(),
|
|
},
|
|
});
|
|
```
|
|
|
|
#### 3. **updateTemplate()** - 템플릿 수정
|
|
|
|
```typescript
|
|
// Line 164
|
|
return await prisma.template_standards.update({
|
|
where: {
|
|
template_code_company_code: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
},
|
|
data: {
|
|
...data,
|
|
updated_date: new Date(),
|
|
},
|
|
});
|
|
```
|
|
|
|
#### 4. **deleteTemplate()** - 템플릿 삭제
|
|
|
|
```typescript
|
|
// Line 181
|
|
await prisma.template_standards.delete({
|
|
where: {
|
|
template_code_company_code: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
#### 5. **getTemplateCategories()** - 카테고리 목록 (DISTINCT)
|
|
|
|
```typescript
|
|
// Line 262
|
|
const categories = await prisma.template_standards.findMany({
|
|
where: {
|
|
company_code: companyCode,
|
|
},
|
|
select: {
|
|
category: true,
|
|
},
|
|
distinct: ["category"],
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 전환 계획
|
|
|
|
### 1단계: 기본 CRUD 전환 (4개 함수)
|
|
|
|
**함수 목록**:
|
|
|
|
- `getTemplateByCode()` - 단건 조회 (findUnique)
|
|
- `createTemplate()` - 생성 (findUnique + create)
|
|
- `updateTemplate()` - 수정 (update)
|
|
- `deleteTemplate()` - 삭제 (delete)
|
|
|
|
### 2단계: 추가 기능 전환 (1개 함수)
|
|
|
|
**함수 목록**:
|
|
|
|
- `getTemplateCategories()` - 카테고리 목록 (findMany + distinct)
|
|
|
|
---
|
|
|
|
## 💻 전환 예시
|
|
|
|
### 예시 1: 복합 키 조회
|
|
|
|
```typescript
|
|
// 기존 Prisma
|
|
return await prisma.template_standards.findUnique({
|
|
where: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
});
|
|
|
|
// 전환 후
|
|
import { queryOne } from "../database/db";
|
|
|
|
return await queryOne<any>(
|
|
`SELECT * FROM template_standards
|
|
WHERE template_code = $1 AND company_code = $2`,
|
|
[templateCode, companyCode]
|
|
);
|
|
```
|
|
|
|
### 예시 2: 중복 확인 후 생성
|
|
|
|
```typescript
|
|
// 기존 Prisma
|
|
const existing = await prisma.template_standards.findUnique({
|
|
where: {
|
|
template_code: data.template_code,
|
|
company_code: data.company_code,
|
|
},
|
|
});
|
|
|
|
if (existing) {
|
|
throw new Error("이미 존재하는 템플릿 코드입니다.");
|
|
}
|
|
|
|
return await prisma.template_standards.create({
|
|
data: {
|
|
...data,
|
|
created_date: new Date(),
|
|
updated_date: new Date(),
|
|
},
|
|
});
|
|
|
|
// 전환 후
|
|
const existing = await queryOne<any>(
|
|
`SELECT * FROM template_standards
|
|
WHERE template_code = $1 AND company_code = $2`,
|
|
[data.template_code, data.company_code]
|
|
);
|
|
|
|
if (existing) {
|
|
throw new Error("이미 존재하는 템플릿 코드입니다.");
|
|
}
|
|
|
|
return await queryOne<any>(
|
|
`INSERT INTO template_standards
|
|
(template_code, template_name, category, template_type, layout_config,
|
|
description, is_active, company_code, created_by, updated_by,
|
|
created_date, updated_date)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
|
|
RETURNING *`,
|
|
[
|
|
data.template_code,
|
|
data.template_name,
|
|
data.category,
|
|
data.template_type,
|
|
JSON.stringify(data.layout_config),
|
|
data.description,
|
|
data.is_active,
|
|
data.company_code,
|
|
data.created_by,
|
|
data.updated_by,
|
|
]
|
|
);
|
|
```
|
|
|
|
### 예시 3: 복합 키 UPDATE
|
|
|
|
```typescript
|
|
// 기존 Prisma
|
|
return await prisma.template_standards.update({
|
|
where: {
|
|
template_code_company_code: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
},
|
|
data: {
|
|
...data,
|
|
updated_date: new Date(),
|
|
},
|
|
});
|
|
|
|
// 전환 후
|
|
// 동적 UPDATE 쿼리 생성
|
|
const updateFields: string[] = ["updated_date = NOW()"];
|
|
const values: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (data.template_name !== undefined) {
|
|
updateFields.push(`template_name = $${paramIndex++}`);
|
|
values.push(data.template_name);
|
|
}
|
|
if (data.category !== undefined) {
|
|
updateFields.push(`category = $${paramIndex++}`);
|
|
values.push(data.category);
|
|
}
|
|
if (data.template_type !== undefined) {
|
|
updateFields.push(`template_type = $${paramIndex++}`);
|
|
values.push(data.template_type);
|
|
}
|
|
if (data.layout_config !== undefined) {
|
|
updateFields.push(`layout_config = $${paramIndex++}`);
|
|
values.push(JSON.stringify(data.layout_config));
|
|
}
|
|
if (data.description !== undefined) {
|
|
updateFields.push(`description = $${paramIndex++}`);
|
|
values.push(data.description);
|
|
}
|
|
if (data.is_active !== undefined) {
|
|
updateFields.push(`is_active = $${paramIndex++}`);
|
|
values.push(data.is_active);
|
|
}
|
|
if (data.updated_by !== undefined) {
|
|
updateFields.push(`updated_by = $${paramIndex++}`);
|
|
values.push(data.updated_by);
|
|
}
|
|
|
|
return await queryOne<any>(
|
|
`UPDATE template_standards
|
|
SET ${updateFields.join(", ")}
|
|
WHERE template_code = $${paramIndex++} AND company_code = $${paramIndex}
|
|
RETURNING *`,
|
|
[...values, templateCode, companyCode]
|
|
);
|
|
```
|
|
|
|
### 예시 4: 복합 키 DELETE
|
|
|
|
```typescript
|
|
// 기존 Prisma
|
|
await prisma.template_standards.delete({
|
|
where: {
|
|
template_code_company_code: {
|
|
template_code: templateCode,
|
|
company_code: companyCode,
|
|
},
|
|
},
|
|
});
|
|
|
|
// 전환 후
|
|
import { query } from "../database/db";
|
|
|
|
await query(
|
|
`DELETE FROM template_standards
|
|
WHERE template_code = $1 AND company_code = $2`,
|
|
[templateCode, companyCode]
|
|
);
|
|
```
|
|
|
|
### 예시 5: DISTINCT 쿼리
|
|
|
|
```typescript
|
|
// 기존 Prisma
|
|
const categories = await prisma.template_standards.findMany({
|
|
where: {
|
|
company_code: companyCode,
|
|
},
|
|
select: {
|
|
category: true,
|
|
},
|
|
distinct: ["category"],
|
|
});
|
|
|
|
return categories
|
|
.map((c) => c.category)
|
|
.filter((c): c is string => c !== null && c !== undefined)
|
|
.sort();
|
|
|
|
// 전환 후
|
|
const categories = await query<{ category: string }>(
|
|
`SELECT DISTINCT category
|
|
FROM template_standards
|
|
WHERE company_code = $1 AND category IS NOT NULL
|
|
ORDER BY category ASC`,
|
|
[companyCode]
|
|
);
|
|
|
|
return categories.map((c) => c.category);
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ 완료 기준
|
|
|
|
- [ ] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
|
|
- [ ] **복합 기본 키 처리 (template_code + company_code)**
|
|
- [ ] **동적 UPDATE 쿼리 생성**
|
|
- [ ] **DISTINCT 쿼리 전환**
|
|
- [ ] **JSON 필드 처리 (layout_config)**
|
|
- [ ] **모든 TypeScript 컴파일 오류 해결**
|
|
- [ ] **`import prisma` 완전 제거**
|
|
- [ ] **모든 단위 테스트 통과 (6개)**
|
|
- [ ] **통합 테스트 작성 완료 (2개 시나리오)**
|
|
|
|
---
|
|
|
|
## 🔧 주요 기술적 과제
|
|
|
|
### 1. 복합 기본 키
|
|
|
|
`template_standards` 테이블은 `(template_code, company_code)` 복합 기본 키를 사용합니다.
|
|
|
|
- WHERE 절에서 두 컬럼 모두 지정 필요
|
|
- Prisma의 `template_code_company_code` 표현식을 `template_code = $1 AND company_code = $2`로 변환
|
|
|
|
### 2. JSON 필드
|
|
|
|
`layout_config` 필드는 JSON 타입으로, INSERT/UPDATE 시 `JSON.stringify()` 필요합니다.
|
|
|
|
### 3. DISTINCT + NULL 제외
|
|
|
|
카테고리 목록 조회 시 `DISTINCT` 사용하며, NULL 값은 `WHERE category IS NOT NULL`로 제외합니다.
|
|
|
|
---
|
|
|
|
## 📋 체크리스트
|
|
|
|
### 코드 전환
|
|
|
|
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
|
|
- [ ] getTemplateByCode() - findUnique → queryOne (복합 키)
|
|
- [ ] createTemplate() - findUnique + create → queryOne (중복 확인 + INSERT)
|
|
- [ ] updateTemplate() - update → queryOne (동적 UPDATE, 복합 키)
|
|
- [ ] deleteTemplate() - delete → query (복합 키)
|
|
- [ ] getTemplateCategories() - findMany + distinct → query (DISTINCT)
|
|
- [ ] JSON 필드 처리 (layout_config)
|
|
- [ ] Prisma import 완전 제거
|
|
|
|
### 테스트
|
|
|
|
- [ ] 단위 테스트 작성 (6개)
|
|
- [ ] 통합 테스트 작성 (2개)
|
|
- [ ] TypeScript 컴파일 성공
|
|
- [ ] 성능 벤치마크 테스트
|
|
|
|
---
|
|
|
|
## 💡 특이사항
|
|
|
|
### 복합 기본 키 패턴
|
|
|
|
이 서비스는 `(template_code, company_code)` 복합 기본 키를 사용하므로, 모든 조회/수정/삭제 작업에서 두 컬럼을 모두 WHERE 조건에 포함해야 합니다.
|
|
|
|
### JSON 레이아웃 설정
|
|
|
|
`layout_config` 필드는 템플릿의 레이아웃 설정을 JSON 형태로 저장합니다. Raw Query 전환 시 `JSON.stringify()`를 사용하여 문자열로 변환해야 합니다.
|
|
|
|
### 카테고리 관리
|
|
|
|
템플릿은 카테고리별로 분류되며, `getTemplateCategories()` 메서드로 고유한 카테고리 목록을 조회할 수 있습니다.
|
|
|
|
---
|
|
|
|
**작성일**: 2025-10-01
|
|
**예상 소요 시간**: 45분
|
|
**담당자**: 백엔드 개발팀
|
|
**우선순위**: 🟢 낮음 (Phase 3.9)
|
|
**상태**: ⏳ **대기 중**
|
|
**특이사항**: 복합 기본 키, JSON 필드, DISTINCT 쿼리 포함
|