feat: Phase 3.12 ExternalCallConfigService Raw Query 전환 완료

외부 호출 설정 관리 서비스의 모든 Prisma 호출을 Raw Query로 전환:

## 전환 완료 (8개 Prisma 호출)

1. **getConfigs()** - 목록 조회
   - prisma.findMany → query<ExternalCallConfig>()
   - 동적 WHERE 조건 (5개 필터)
   - ILIKE 검색 (config_name, description)

2. **getConfigById()** - 단건 조회
   - prisma.findUnique → queryOne<ExternalCallConfig>()

3-4. **createConfig()** - 생성
   - 중복 검사: prisma.findFirst → queryOne()
   - 생성: prisma.create → queryOne() with INSERT RETURNING
   - JSON 필드 처리: config_data

5-6. **updateConfig()** - 수정
   - 중복 검사: prisma.findFirst → queryOne() with id != $4
   - 수정: prisma.update → queryOne() with 동적 UPDATE
   - 9개 필드에 대한 조건부 SET 절 생성

7. **deleteConfig()** - 논리 삭제
   - prisma.update → query() with is_active = 'N'

8. **getExternalCallConfigsForButtonControl()** - 버튼 제어용
   - prisma.findMany with select → query() with SELECT

## 기술적 개선사항

- **동적 WHERE 조건**: 5개 필터 조건 조합 및 파라미터 인덱싱
- **동적 UPDATE 쿼리**: 변경된 필드만 포함하는 SET 절 생성
- **JSON 필드**: config_data를 JSON.stringify()로 처리
- **ILIKE 검색**: 대소문자 구분 없는 검색 구현
- **중복 검사**: id 제외 조건으로 자신 제외 로직 유지

## 코드 정리

- prisma import 완전 제거
- query, queryOne 함수 사용
- 컴파일 및 린터 오류 없음

문서: PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md
진행률: Phase 3 136/162 (84.0%)
This commit is contained in:
kjs
2025-10-01 12:07:14 +09:00
parent 510c7b2416
commit b4b4c774fb
3 changed files with 202 additions and 107 deletions

View File

@@ -6,15 +6,15 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | --------------------------------------------------------- |
| 항목 | 내용 |
| --------------- | -------------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/externalCallConfigService.ts` |
| 파일 크기 | 581 라인 |
| Prisma 호출 | 8 |
| **현재 진행률** | **0/8 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) |
| 우선순위 | 🟡 중간 (Phase 3.12) |
| **상태** | **대기 중** |
| 파일 크기 | 612 라인 |
| Prisma 호출 | 0(전환 완료) |
| **현재 진행률** | **8/8 (100%)** **전환 완료** |
| 복잡도 | 중간 (JSON 필드, 복잡한 CRUD) |
| 우선순위 | 🟡 중간 (Phase 3.12) |
| **상태** | **완료** |
### 🎯 전환 목표
@@ -33,35 +33,43 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
### 주요 기능 (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 관계 조인
- 통계 쿼리
@@ -70,6 +78,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
## 💡 전환 전략
### 1단계: 기본 CRUD 전환 (5개)
- getExternalCallConfigs() - 목록 조회
- getExternalCallConfig() - 단건 조회
- createExternalCallConfig() - 생성
@@ -77,6 +86,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
- deleteExternalCallConfig() - 삭제
### 2단계: 추가 기능 전환 (3개)
- duplicateExternalCallConfig() - 복제
- testExternalCallConfig() - 테스트
- getExternalCallHistory() - 이력 조회
@@ -88,6 +98,7 @@ ExternalCallConfigService는 **8개의 Prisma 호출**이 있으며, 외부 API
### 예시 1: 목록 조회 (동적 WHERE + JSON)
**변경 전**:
```typescript
const configs = await prisma.external_call_configs.findMany({
where: {
@@ -105,6 +116,7 @@ const configs = await prisma.external_call_configs.findMany({
```
**변경 후**:
```typescript
const conditions: string[] = ["company_code = $1"];
const params: any[] = [companyCode];
@@ -135,6 +147,7 @@ const configs = await query<any>(
### 예시 2: JSON 필드 생성
**변경 전**:
```typescript
const config = await prisma.external_call_configs.create({
data: {
@@ -150,6 +163,7 @@ const config = await prisma.external_call_configs.create({
```
**변경 후**:
```typescript
const config = await queryOne<any>(
`INSERT INTO external_call_configs
@@ -172,6 +186,7 @@ const config = await queryOne<any>(
### 예시 3: 동적 UPDATE (JSON 포함)
**변경 전**:
```typescript
const updateData: any = {};
if (data.headers) updateData.headers = data.headers;
@@ -184,6 +199,7 @@ const config = await prisma.external_call_configs.update({
```
**변경 후**:
```typescript
const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
@@ -213,23 +229,26 @@ const config = await queryOne<any>(
## 🔧 기술적 고려사항
### 1. JSON 필드 처리
3개의 JSON 필드가 있을 것으로 예상:
- `headers` - HTTP 헤더
- `params` - 쿼리 파라미터
- `auth_config` - 인증 설정 (암호화됨)
```typescript
// INSERT/UPDATE 시
JSON.stringify(jsonData)
JSON.stringify(jsonData);
// SELECT 후
const parsedData = typeof row.headers === 'string'
? JSON.parse(row.headers)
: row.headers;
const parsedData =
typeof row.headers === "string" ? JSON.parse(row.headers) : row.headers;
```
### 2. 민감 정보 암호화
auth_config는 암호화되어 저장되므로, 기존 암호화/복호화 로직 유지:
```typescript
import { encrypt, decrypt } from "../utils/encryption";
@@ -241,6 +260,7 @@ 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)) {
@@ -249,6 +269,7 @@ if (!VALID_HTTP_METHODS.includes(httpMethod)) {
```
### 4. URL 검증
```typescript
try {
new URL(endpointUrl);
@@ -259,9 +280,38 @@ try {
---
## 📝 전환 체크리스트
## 전환 완료 내역
### 전환된 Prisma 호출 (8개)
1. **`getConfigs()`** - 목록 조회 (findMany → query)
2. **`getConfigById()`** - 단건 조회 (findUnique → queryOne)
3. **`createConfig()`** - 중복 검사 (findFirst → queryOne)
4. **`createConfig()`** - 생성 (create → queryOne with INSERT)
5. **`updateConfig()`** - 중복 검사 (findFirst → queryOne)
6. **`updateConfig()`** - 수정 (update → queryOne with 동적 UPDATE)
7. **`deleteConfig()`** - 삭제 (update → query)
8. **`getExternalCallConfigsForButtonControl()`** - 조회 (findMany → query)
### 주요 기술적 개선사항
- 동적 WHERE 조건 생성 (company_code, call_type, api_type, is_active, search)
- ILIKE를 활용한 대소문자 구분 없는 검색
- 동적 UPDATE 쿼리 (9개 필드)
- JSON 필드 처리 (`config_data``JSON.stringify()`)
- 중복 검사 로직 유지
### 코드 정리
- [x] import 문 수정 완료
- [x] Prisma import 완전 제거
- [x] TypeScript 컴파일 성공
- [x] Linter 오류 없음
## 📝 원본 전환 체크리스트
### 1단계: Prisma 호출 전환 (✅ 완료)
### 1단계: Prisma 호출 전환
- [ ] `getExternalCallConfigs()` - 목록 조회 (findMany + count)
- [ ] `getExternalCallConfig()` - 단건 조회 (findUnique)
- [ ] `createExternalCallConfig()` - 생성 (create)
@@ -272,18 +322,21 @@ try {
- [ ] `getExternalCallHistory()` - 이력 조회 (findMany)
### 2단계: 코드 정리
- [ ] import 문 수정 (`prisma``query, queryOne`)
- [ ] JSON 필드 처리 확인
- [ ] 암호화/복호화 로직 유지
- [ ] Prisma import 완전 제거
### 3단계: 테스트
- [ ] 단위 테스트 작성 (8개)
- [ ] 통합 테스트 작성 (3개)
- [ ] 암호화 테스트
- [ ] HTTP 호출 테스트
### 4단계: 문서화
- [ ] 전환 완료 문서 업데이트
- [ ] API 문서 업데이트
@@ -295,11 +348,9 @@ try {
- JSON 필드 처리
- 암호화/복호화 로직
- HTTP 호출 테스트
- **예상 소요 시간**: 1~1.5시간
---
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드, 민감 정보 암호화, HTTP 호출 포함

View File

@@ -136,7 +136,7 @@ backend-node/ (루트)
#### 🟡 **중간 (단순 CRUD) - 3순위**
- `ddlAuditLogger.ts` (0개) - ✅ **전환 완료** (Phase 3.11) - [계획서](PHASE3.11_DDL_AUDIT_LOGGER_MIGRATION.md)
- `externalCallConfigService.ts` (8개) - 외부 호출 설정 - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md)
- `externalCallConfigService.ts` (0개) - ✅ **전환 완료** (Phase 3.12) - [계획서](PHASE3.12_EXTERNAL_CALL_CONFIG_SERVICE_MIGRATION.md)
- `entityJoinService.ts` (5개) - 엔티티 조인 - [계획서](PHASE3.13_ENTITY_JOIN_SERVICE_MIGRATION.md)
- `authService.ts` (5개) - 사용자 인증 - [계획서](PHASE3.14_AUTH_SERVICE_MIGRATION.md)
- **배치 관련 서비스 (24개)** - [통합 계획서](PHASE3.15_BATCH_SERVICES_MIGRATION.md)

View File

@@ -1,4 +1,4 @@
import prisma from "../config/database";
import { query, queryOne } from "../database/db";
import { logger } from "../utils/logger";
// 외부 호출 설정 타입 정의
@@ -34,43 +34,55 @@ export class ExternalCallConfigService {
logger.info("=== 외부 호출 설정 목록 조회 시작 ===");
logger.info(`필터 조건:`, filter);
const where: any = {};
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
// 회사 코드 필터
if (filter.company_code) {
where.company_code = filter.company_code;
conditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
}
// 호출 타입 필터
if (filter.call_type) {
where.call_type = filter.call_type;
conditions.push(`call_type = $${paramIndex++}`);
params.push(filter.call_type);
}
// API 타입 필터
if (filter.api_type) {
where.api_type = filter.api_type;
conditions.push(`api_type = $${paramIndex++}`);
params.push(filter.api_type);
}
// 활성화 상태 필터
if (filter.is_active) {
where.is_active = filter.is_active;
conditions.push(`is_active = $${paramIndex++}`);
params.push(filter.is_active);
}
// 검색어 필터 (설정 이름 또는 설명)
if (filter.search) {
where.OR = [
{ config_name: { contains: filter.search, mode: "insensitive" } },
{ description: { contains: filter.search, mode: "insensitive" } },
];
conditions.push(
`(config_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
params.push(`%${filter.search}%`);
paramIndex++;
}
const configs = await prisma.external_call_configs.findMany({
where,
orderBy: [{ is_active: "desc" }, { created_date: "desc" }],
});
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const configs = await query<ExternalCallConfig>(
`SELECT * FROM external_call_configs
${whereClause}
ORDER BY is_active DESC, created_date DESC`,
params
);
logger.info(`외부 호출 설정 조회 결과: ${configs.length}`);
return configs as ExternalCallConfig[];
return configs;
} catch (error) {
logger.error("외부 호출 설정 목록 조회 실패:", error);
throw error;
@@ -84,9 +96,10 @@ export class ExternalCallConfigService {
try {
logger.info(`=== 외부 호출 설정 조회: ID ${id} ===`);
const config = await prisma.external_call_configs.findUnique({
where: { id },
});
const config = await queryOne<ExternalCallConfig>(
`SELECT * FROM external_call_configs WHERE id = $1`,
[id]
);
if (config) {
logger.info(`외부 호출 설정 조회 성공: ${config.config_name}`);
@@ -94,7 +107,7 @@ export class ExternalCallConfigService {
logger.warn(`외부 호출 설정을 찾을 수 없음: ID ${id}`);
}
return config as ExternalCallConfig | null;
return config || null;
} catch (error) {
logger.error(`외부 호출 설정 조회 실패 (ID: ${id}):`, error);
throw error;
@@ -115,13 +128,11 @@ export class ExternalCallConfigService {
});
// 중복 이름 검사
const existingConfig = await prisma.external_call_configs.findFirst({
where: {
config_name: data.config_name,
company_code: data.company_code || "*",
is_active: "Y",
},
});
const existingConfig = await queryOne<ExternalCallConfig>(
`SELECT * FROM external_call_configs
WHERE config_name = $1 AND company_code = $2 AND is_active = $3`,
[data.config_name, data.company_code || "*", "Y"]
);
if (existingConfig) {
throw new Error(
@@ -129,24 +140,29 @@ export class ExternalCallConfigService {
);
}
const newConfig = await prisma.external_call_configs.create({
data: {
config_name: data.config_name,
call_type: data.call_type,
api_type: data.api_type,
config_data: data.config_data,
description: data.description,
company_code: data.company_code || "*",
is_active: data.is_active || "Y",
created_by: data.created_by,
updated_by: data.updated_by,
},
});
const newConfig = await queryOne<ExternalCallConfig>(
`INSERT INTO external_call_configs
(config_name, call_type, api_type, config_data, description,
company_code, is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING *`,
[
data.config_name,
data.call_type,
data.api_type,
JSON.stringify(data.config_data),
data.description,
data.company_code || "*",
data.is_active || "Y",
data.created_by,
data.updated_by,
]
);
logger.info(
`외부 호출 설정 생성 완료: ${newConfig.config_name} (ID: ${newConfig.id})`
`외부 호출 설정 생성 완료: ${newConfig!.config_name} (ID: ${newConfig!.id})`
);
return newConfig as ExternalCallConfig;
return newConfig!;
} catch (error) {
logger.error("외부 호출 설정 생성 실패:", error);
throw error;
@@ -171,14 +187,16 @@ export class ExternalCallConfigService {
// 이름 중복 검사 (다른 설정과 중복되는지)
if (data.config_name && data.config_name !== existingConfig.config_name) {
const duplicateConfig = await prisma.external_call_configs.findFirst({
where: {
config_name: data.config_name,
company_code: data.company_code || existingConfig.company_code,
is_active: "Y",
id: { not: id },
},
});
const duplicateConfig = await queryOne<ExternalCallConfig>(
`SELECT * FROM external_call_configs
WHERE config_name = $1 AND company_code = $2 AND is_active = $3 AND id != $4`,
[
data.config_name,
data.company_code || existingConfig.company_code,
"Y",
id,
]
);
if (duplicateConfig) {
throw new Error(
@@ -187,27 +205,58 @@ export class ExternalCallConfigService {
}
}
const updatedConfig = await prisma.external_call_configs.update({
where: { id },
data: {
...(data.config_name && { config_name: data.config_name }),
...(data.call_type && { call_type: data.call_type }),
...(data.api_type !== undefined && { api_type: data.api_type }),
...(data.config_data && { config_data: data.config_data }),
...(data.description !== undefined && {
description: data.description,
}),
...(data.company_code && { company_code: data.company_code }),
...(data.is_active && { is_active: data.is_active }),
...(data.updated_by && { updated_by: data.updated_by }),
updated_date: new Date(),
},
});
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_date = NOW()"];
const params: any[] = [];
let paramIndex = 1;
if (data.config_name) {
updateFields.push(`config_name = $${paramIndex++}`);
params.push(data.config_name);
}
if (data.call_type) {
updateFields.push(`call_type = $${paramIndex++}`);
params.push(data.call_type);
}
if (data.api_type !== undefined) {
updateFields.push(`api_type = $${paramIndex++}`);
params.push(data.api_type);
}
if (data.config_data) {
updateFields.push(`config_data = $${paramIndex++}`);
params.push(JSON.stringify(data.config_data));
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
params.push(data.description);
}
if (data.company_code) {
updateFields.push(`company_code = $${paramIndex++}`);
params.push(data.company_code);
}
if (data.is_active) {
updateFields.push(`is_active = $${paramIndex++}`);
params.push(data.is_active);
}
if (data.updated_by) {
updateFields.push(`updated_by = $${paramIndex++}`);
params.push(data.updated_by);
}
params.push(id);
const updatedConfig = await queryOne<ExternalCallConfig>(
`UPDATE external_call_configs
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
RETURNING *`,
params
);
logger.info(
`외부 호출 설정 수정 완료: ${updatedConfig.config_name} (ID: ${id})`
`외부 호출 설정 수정 완료: ${updatedConfig!.config_name} (ID: ${id})`
);
return updatedConfig as ExternalCallConfig;
return updatedConfig!;
} catch (error) {
logger.error(`외부 호출 설정 수정 실패 (ID: ${id}):`, error);
throw error;
@@ -228,14 +277,12 @@ export class ExternalCallConfigService {
}
// 논리 삭제 (is_active = 'N')
await prisma.external_call_configs.update({
where: { id },
data: {
is_active: "N",
updated_by: deletedBy,
updated_date: new Date(),
},
});
await query(
`UPDATE external_call_configs
SET is_active = $1, updated_by = $2, updated_date = NOW()
WHERE id = $3`,
["N", deletedBy, id]
);
logger.info(
`외부 호출 설정 삭제 완료: ${existingConfig.config_name} (ID: ${id})`
@@ -401,21 +448,18 @@ export class ExternalCallConfigService {
}>
> {
try {
const configs = await prisma.external_call_configs.findMany({
where: {
company_code: companyCode,
is_active: "Y",
},
select: {
id: true,
config_name: true,
description: true,
config_data: true,
},
orderBy: {
config_name: "asc",
},
});
const configs = await query<{
id: number;
config_name: string;
description: string | null;
config_data: any;
}>(
`SELECT id, config_name, description, config_data
FROM external_call_configs
WHERE company_code = $1 AND is_active = $2
ORDER BY config_name ASC`,
[companyCode, "Y"]
);
return configs.map((config) => {
const configData = config.config_data as any;