Compare commits

..

No commits in common. "4c20d93c8708bd420ff8579d4a8f3da4a82cc675" and "57f1d8274e6409c63951e0bd7c87badb88e11f06" have entirely different histories.

16 changed files with 1494 additions and 3489 deletions

View File

@ -6,16 +6,15 @@ DynamicFormService는 **13개의 Prisma 호출**이 있습니다. 대부분(약
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dynamicFormService.ts` |
| 파일 크기 | 1,213 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **13/13 (100%)****완료** |
| **전환 상태** | **Raw Query로 전환 완료** |
| 복잡도 | 낮음 (SQL 작성 완료 → `query()` 함수로 교체 완료) |
| 우선순위 | 🟢 낮음 (Phase 2.4) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
| 항목 | 내용 |
| --------------- | ---------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dynamicFormService.ts` |
| 파일 크기 | 1,200+ 라인 |
| Prisma 호출 | 13개 ($queryRaw: 11개, ORM: 2개) |
| **현재 진행률** | **0/13 (0%)****전환 필요** |
| **전환 필요** | **13개 모두 전환 필요** (SQL은 이미 작성되어 있음) |
| 복잡도 | 낮음 (SQL 작성은 완료, `query()` 함수로 교체만 필요) |
| 우선순위 | 🟢 낮음 (Phase 2.4) |
### 🎯 전환 목표
@ -168,63 +167,52 @@ describe("동적 폼 통합 테스트", () => {
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (13개 Raw Query 호출)
1. **getTableColumnInfo()** - 컬럼 정보 조회
2. **getPrimaryKeyColumns()** - 기본 키 조회
3. **getNotNullColumns()** - NOT NULL 컬럼 조회
4. **upsertFormData()** - UPSERT 실행
5. **partialUpdateFormData()** - 부분 업데이트
6. **updateFormData()** - 전체 업데이트
7. **deleteFormData()** - 데이터 삭제
8. **getFormDataById()** - 폼 데이터 조회
9. **getTableColumns()** - 테이블 컬럼 조회
10. **getTablePrimaryKeys()** - 기본 키 조회
11. **getScreenLayoutsForControl()** - 화면 레이아웃 조회
### 🔧 주요 기술적 해결 사항
1. **Prisma import 완전 제거**: `import { query, queryOne } from "../database/db"`
2. **동적 UPSERT 쿼리**: PostgreSQL ON CONFLICT 구문 사용
3. **부분 업데이트**: 동적 SET 절 생성
4. **타입 변환**: PostgreSQL 타입 자동 변환 로직 유지
## 📋 체크리스트
### 1단계: ORM 호출 전환 ✅ **완료**
### 1단계: ORM 호출 전환 (2개 함수) ⏳ **진행 예정**
- [x] `getFormDataById()` - queryOne 전환
- [x] `getScreenLayoutsForControl()` - query 전환
- [x] 모든 Raw Query 함수 전환
- [ ] `getFormDataById()` - dynamic_form_data.findUnique
- [ ] `getScreenLayoutsForControl()` - screen_layouts.findMany
### 2단계: 테스트 & 검증 ⏳ **진행 예정**
- [ ] 단위 테스트 작성 (5개)
- [ ] 통합 테스트 작성 (3개 시나리오)
- [x] Prisma import 완전 제거 확인 ✅
- [ ] Prisma import 완전 제거 확인
- [ ] 성능 테스트
---
## 🎯 완료 기준
- [x] **13개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [x] 11개 `$queryRaw``query()` 함수로 교체 ✅
- [x] 2개 ORM 메서드 → `query()` 함수로 전환 ✅
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **`import prisma` 완전 제거** ✅
- [ ] **모든 단위 테스트 통과 (5개)**
- [ ] **모든 통합 테스트 작성 완료 (3개 시나리오)**
- [ ] **성능 저하 없음**
- [ ] **13개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] 11개 `$queryRaw``query()` 함수로 교체
- [ ] 2개 ORM 메서드 → `query()` 함수로 전환 (SQL 작성)
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **모든 단위 테스트 통과 (5개)**
- [ ] **모든 통합 테스트 작성 완료 (3개 시나리오)**
- [ ] **`import prisma` 완전 제거 및 `import { query } from "../database/db"` 사용**
- [ ] **성능 저하 없음**
---
## 💡 특이사항
### SQL은 이미 거의 작성되어 있음
이 서비스는 이미 85%가 `$queryRaw`를 사용하고 있어, **SQL 작성은 거의 완료**되었습니다:
- ✅ UPSERT 로직: SQL 작성 완료 (`$queryRawUnsafe` 사용 중)
- ✅ 컬럼 정보 조회: SQL 작성 완료 (`$queryRaw` 사용 중)
- ✅ Primary Key 조회: SQL 작성 완료 (Raw Query 사용 중)
- ⏳ **전환 작업**: `prisma.$queryRaw``query()` 함수로 **단순 교체만 필요**
- ⏳ 단순 조회: 2개만 SQL 새로 작성 필요 (매우 간단한 SELECT 쿼리)
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 완료됨 (이전에 전환)
**예상 소요 시간**: 0.5일 (SQL은 85% 작성 완료, 함수 교체 작업 필요)
**담당자**: 백엔드 개발팀
**우선순위**: 🟢 낮음 (Phase 2.4)
**상태**: ✅ **전환 완료** (테스트 필요)
**특이사항**: SQL은 이미 작성되어 있었고, `query()` 함수로 교체 완료
**상태**: ⏳ **진행 예정**
**특이사항**: SQL은 거의 작성되어 있어 `prisma.$queryRaw``query()` 단순 교체 작업이 주요 작업

View File

@ -9,12 +9,11 @@ ExternalDbConnectionService는 **15개의 Prisma 호출**이 있으며, 외부
| 항목 | 내용 |
| --------------- | ---------------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/externalDbConnectionService.ts` |
| 파일 크기 | 1,100+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **15/15 (100%)** ✅ **완료** |
| 파일 크기 | 800+ 라인 |
| Prisma 호출 | 15개 |
| **현재 진행률** | **0/15 (0%)** ⏳ **진행 예정** |
| 복잡도 | 중간 (CRUD + 연결 테스트) |
| 우선순위 | 🟡 중간 (Phase 2.5) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
@ -83,43 +82,18 @@ await query(
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (15개 Prisma 호출)
1. **getConnections()** - 동적 WHERE 조건 생성으로 전환
2. **getConnectionsGroupedByType()** - DB 타입 카테고리 조회
3. **getConnectionById()** - 단일 연결 조회 (비밀번호 마스킹)
4. **getConnectionByIdWithPassword()** - 비밀번호 포함 조회
5. **createConnection()** - 새 연결 생성 + 중복 확인
6. **updateConnection()** - 동적 필드 업데이트
7. **deleteConnection()** - 물리 삭제
8. **testConnectionById()** - 연결 테스트용 조회
9. **getDecryptedPassword()** - 비밀번호 복호화용 조회
10. **executeQuery()** - 쿼리 실행용 조회
11. **getTables()** - 테이블 목록 조회용
### 🔧 주요 기술적 해결 사항
1. **동적 WHERE 조건 생성**: 필터 조건에 따라 동적으로 SQL 생성
2. **동적 UPDATE 쿼리**: 변경된 필드만 업데이트하도록 구현
3. **ILIKE 검색**: 대소문자 구분 없는 검색 지원
4. **암호화 로직 유지**: PasswordEncryption 클래스와 통합 유지
## 🎯 완료 기준
- [x] **15개 Prisma 호출 모두 Raw Query로 전환**
- [x] **암호화/복호화 로직 정상 동작**
- [x] **연결 테스트 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개 이상)**
- [x] **Prisma import 완전 제거**
- [x] **TypeScript 컴파일 성공**
- [ ] **15개 Prisma 호출 모두 Raw Query로 전환**
- [ ] **암호화/복호화 로직 정상 동작**
- [ ] **연결 테스트 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개 이상)**
- [ ] **Prisma import 완전 제거**
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 1시간
**예상 소요 시간**: 1일
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 2.5)
**상태**: **전환 완료** (테스트 필요)
**상태**: ⏳ **진행 예정**

View File

@ -9,12 +9,11 @@ DataflowControlService는 **6개의 Prisma 호출**이 있으며, 데이터플
| 항목 | 내용 |
| --------------- | ----------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/dataflowControlService.ts` |
| 파일 크기 | 1,100+ 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| **현재 진행률** | **6/6 (100%)** ✅ **완료** |
| 파일 크기 | 600+ 라인 |
| Prisma 호출 | 6개 |
| **현재 진행률** | **0/6 (0%)** ⏳ **진행 예정** |
| 복잡도 | 높음 (복잡한 비즈니스 로직) |
| 우선순위 | 🟡 중간 (Phase 2.6) |
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
### 🎯 전환 목표
@ -164,34 +163,16 @@ await transaction(async (client) => {
---
## 📋 전환 완료 내역
### ✅ 전환된 함수들 (6개 Prisma 호출)
1. **executeDataflowControl()** - 관계도 정보 조회 (findUnique → queryOne)
2. **evaluateActionConditions()** - 대상 테이블 조건 확인 ($queryRawUnsafe → query)
3. **executeInsertAction()** - INSERT 실행 ($executeRawUnsafe → query)
4. **executeUpdateAction()** - UPDATE 실행 ($executeRawUnsafe → query)
5. **executeDeleteAction()** - DELETE 실행 ($executeRawUnsafe → query)
6. **checkColumnExists()** - 컬럼 존재 확인 ($queryRawUnsafe → query)
### 🔧 주요 기술적 해결 사항
1. **Prisma import 완전 제거**: `import { query, queryOne } from "../database/db"`
2. **동적 테이블 쿼리 전환**: `$queryRawUnsafe` / `$executeRawUnsafe``query()`
3. **파라미터 바인딩 수정**: MySQL `?` → PostgreSQL `$1, $2...`
4. **복잡한 비즈니스 로직 유지**: 조건부 실행, 다중 커넥션, 에러 처리
## 🎯 완료 기준
- [x] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [x] **모든 TypeScript 컴파일 오류 해결**
- [x] **`import prisma` 완전 제거** ✅
- [ ] **트랜잭션 정상 동작 확인**
- [ ] **복잡한 비즈니스 로직 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **모든 통합 테스트 작성 완료 (4개 시나리오)**
- [ ] **성능 저하 없음**
- [ ] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **트랜잭션 정상 동작 확인**
- [ ] **복잡한 비즈니스 로직 정상 동작**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **모든 통합 테스트 작성 완료 (4개 시나리오)**
- [ ] **`import prisma` 완전 제거 및 `import { query, transaction } from "../database/db"` 사용**
- [ ] **성능 저하 없음**
---
@ -217,9 +198,8 @@ await transaction(async (client) => {
---
**작성일**: 2025-09-30
**완료일**: 2025-10-01
**소요 시간**: 30분
**예상 소요 시간**: 1일
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 2.6)
**상태**: **전환 완료** (테스트 필요)
**상태**: ⏳ **진행 예정**
**특이사항**: 복잡한 비즈니스 로직이 포함되어 있어 신중한 테스트 필요

View File

@ -1,369 +0,0 @@
# 🎨 Phase 3.7: LayoutService Raw Query 전환 계획
## 📋 개요
LayoutService는 **10개의 Prisma 호출**이 있으며, 레이아웃 표준 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | --------------------------------------------- |
| 파일 위치 | `backend-node/src/services/layoutService.ts` |
| 파일 크기 | 425+ 라인 |
| Prisma 호출 | 10개 |
| **현재 진행률** | **0/10 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (JSON 필드, 검색, 통계) |
| 우선순위 | 🟡 중간 (Phase 3.7) |
| **상태** | ⏳ **대기 중** |
### 🎯 전환 목표
- ⏳ **10개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ JSON 필드 처리 (layout_config, sections)
- ⏳ 복잡한 검색 조건 처리
- ⏳ GROUP BY 통계 쿼리 전환
- ⏳ 모든 단위 테스트 통과
- ⏳ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (10개)
#### 1. **getLayouts()** - 레이아웃 목록 조회
```typescript
// Line 92, 102
const total = await prisma.layout_standards.count({ where });
const layouts = await prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: { updated_date: "desc" },
});
```
#### 2. **getLayoutByCode()** - 레이아웃 단건 조회
```typescript
// Line 152
const layout = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
```
#### 3. **createLayout()** - 레이아웃 생성
```typescript
// Line 199
const layout = await prisma.layout_standards.create({
data: {
layout_code,
layout_name,
layout_type,
category,
layout_config: safeJSONStringify(layout_config),
sections: safeJSONStringify(sections),
// ... 기타 필드
},
});
```
#### 4. **updateLayout()** - 레이아웃 수정
```typescript
// Line 230, 267
const existing = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
const updated = await prisma.layout_standards.update({
where: { id: existing.id },
data: { ... },
});
```
#### 5. **deleteLayout()** - 레이아웃 삭제
```typescript
// Line 283, 295
const existing = await prisma.layout_standards.findFirst({
where: { layout_code: code, company_code: companyCode },
});
await prisma.layout_standards.update({
where: { id: existing.id },
data: { is_active: "N", updated_by, updated_date: new Date() },
});
```
#### 6. **getLayoutStatistics()** - 레이아웃 통계
```typescript
// Line 345
const counts = await prisma.layout_standards.groupBy({
by: ["category", "layout_type"],
where: { company_code: companyCode, is_active: "Y" },
_count: { id: true },
});
```
#### 7. **getLayoutCategories()** - 카테고리 목록
```typescript
// Line 373
const existingCodes = await prisma.layout_standards.findMany({
where: { company_code: companyCode },
select: { category: true },
distinct: ["category"],
});
```
---
## 📝 전환 계획
### 1단계: 기본 CRUD 전환 (5개 함수)
**함수 목록**:
- `getLayouts()` - 목록 조회 (count + findMany)
- `getLayoutByCode()` - 단건 조회 (findFirst)
- `createLayout()` - 생성 (create)
- `updateLayout()` - 수정 (findFirst + update)
- `deleteLayout()` - 삭제 (findFirst + update - soft delete)
### 2단계: 통계 및 집계 전환 (2개 함수)
**함수 목록**:
- `getLayoutStatistics()` - 통계 (groupBy)
- `getLayoutCategories()` - 카테고리 목록 (findMany + distinct)
---
## 💻 전환 예시
### 예시 1: 레이아웃 목록 조회 (동적 WHERE + 페이지네이션)
```typescript
// 기존 Prisma
const where: any = { company_code: companyCode };
if (category) where.category = category;
if (layoutType) where.layout_type = layoutType;
if (searchTerm) {
where.OR = [
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
{ layout_code: { contains: searchTerm, mode: "insensitive" } },
];
}
const total = await prisma.layout_standards.count({ where });
const layouts = await prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: { updated_date: "desc" },
});
// 전환 후
import { query, queryOne } from "../database/db";
const whereConditions: string[] = ["company_code = $1"];
const values: any[] = [companyCode];
let paramIndex = 2;
if (category) {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
}
if (layoutType) {
whereConditions.push(`layout_type = $${paramIndex++}`);
values.push(layoutType);
}
if (searchTerm) {
whereConditions.push(
`(layout_name ILIKE $${paramIndex} OR layout_code ILIKE $${paramIndex})`
);
values.push(`%${searchTerm}%`);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
// 총 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM layout_standards ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
// 데이터 조회
const layouts = await query<any>(
`SELECT * FROM layout_standards
${whereClause}
ORDER BY updated_date DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, skip]
);
```
### 예시 2: JSON 필드 처리 (레이아웃 생성)
```typescript
// 기존 Prisma
const layout = await prisma.layout_standards.create({
data: {
layout_code,
layout_name,
layout_config: safeJSONStringify(layout_config), // JSON 필드
sections: safeJSONStringify(sections), // JSON 필드
company_code: companyCode,
created_by: createdBy,
},
});
// 전환 후
const layout = await queryOne<any>(
`INSERT INTO layout_standards
(layout_code, layout_name, layout_type, category, layout_config, sections,
company_code, is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
RETURNING *`,
[
layout_code,
layout_name,
layout_type,
category,
safeJSONStringify(layout_config), // JSON 필드는 문자열로 변환
safeJSONStringify(sections),
companyCode,
"Y",
createdBy,
updatedBy,
]
);
```
### 예시 3: GROUP BY 통계 쿼리
```typescript
// 기존 Prisma
const counts = await prisma.layout_standards.groupBy({
by: ["category", "layout_type"],
where: { company_code: companyCode, is_active: "Y" },
_count: { id: true },
});
// 전환 후
const counts = await query<{
category: string;
layout_type: string;
count: string;
}>(
`SELECT category, layout_type, COUNT(*) as count
FROM layout_standards
WHERE company_code = $1 AND is_active = $2
GROUP BY category, layout_type`,
[companyCode, "Y"]
);
// 결과 포맷팅
const formattedCounts = counts.map((row) => ({
category: row.category,
layout_type: row.layout_type,
_count: { id: parseInt(row.count) },
}));
```
### 예시 4: DISTINCT 쿼리 (카테고리 목록)
```typescript
// 기존 Prisma
const existingCodes = await prisma.layout_standards.findMany({
where: { company_code: companyCode },
select: { category: true },
distinct: ["category"],
});
// 전환 후
const existingCodes = await query<{ category: string }>(
`SELECT DISTINCT category
FROM layout_standards
WHERE company_code = $1
ORDER BY category`,
[companyCode]
);
```
---
## ✅ 완료 기준
- [ ] **10개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **동적 WHERE 조건 생성 (ILIKE, OR)**
- [ ] **JSON 필드 처리 (layout_config, sections)**
- [ ] **GROUP BY 집계 쿼리 전환**
- [ ] **DISTINCT 쿼리 전환**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **`import prisma` 완전 제거**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **통합 테스트 작성 완료 (3개 시나리오)**
---
## 🔧 주요 기술적 과제
### 1. JSON 필드 처리
- `layout_config`, `sections` 필드는 JSON 타입
- INSERT/UPDATE 시 `JSON.stringify()` 또는 `safeJSONStringify()` 사용
- SELECT 시 PostgreSQL이 자동으로 JSON 객체로 반환
### 2. 동적 검색 조건
- category, layoutType, searchTerm에 따른 동적 WHERE 절
- OR 조건 처리 (layout_name OR layout_code)
### 3. Soft Delete
- `deleteLayout()`는 실제 삭제가 아닌 `is_active = 'N'` 업데이트
- UPDATE 쿼리 사용
### 4. 통계 쿼리
- `groupBy``GROUP BY` + `COUNT(*)` 전환
- 결과 포맷팅 필요 (`_count.id` 형태로 변환)
---
## 📋 체크리스트
### 코드 전환
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] getLayouts() - count + findMany → query + queryOne
- [ ] getLayoutByCode() - findFirst → queryOne
- [ ] createLayout() - create → queryOne (INSERT)
- [ ] updateLayout() - findFirst + update → queryOne (동적 UPDATE)
- [ ] deleteLayout() - findFirst + update → queryOne (UPDATE is_active)
- [ ] getLayoutStatistics() - groupBy → query (GROUP BY)
- [ ] getLayoutCategories() - findMany + distinct → query (DISTINCT)
- [ ] JSON 필드 처리 확인 (safeJSONStringify)
- [ ] Prisma import 완전 제거
### 테스트
- [ ] 단위 테스트 작성 (10개)
- [ ] 통합 테스트 작성 (3개)
- [ ] TypeScript 컴파일 성공
- [ ] 성능 벤치마크 테스트
---
## 💡 특이사항
### JSON 필드 헬퍼 함수
이 서비스는 `safeJSONParse()`, `safeJSONStringify()` 헬퍼 함수를 사용하여 JSON 필드를 안전하게 처리합니다. Raw Query 전환 후에도 이 함수들을 계속 사용해야 합니다.
### Soft Delete 패턴
레이아웃 삭제는 실제 DELETE가 아닌 `is_active = 'N'` 업데이트로 처리되므로, UPDATE 쿼리를 사용해야 합니다.
### 통계 쿼리 결과 포맷
Prisma의 `groupBy``_count: { id: number }` 형태로 반환하지만, Raw Query는 `count: string`으로 반환하므로 포맷팅이 필요합니다.
---
**작성일**: 2025-10-01
**예상 소요 시간**: 1시간
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 3.7)
**상태**: ⏳ **대기 중**
**특이사항**: JSON 필드 처리, GROUP BY, DISTINCT 쿼리 포함

View File

@ -1,484 +0,0 @@
# 🗂️ Phase 3.8: DbTypeCategoryService Raw Query 전환 계획
## 📋 개요
DbTypeCategoryService는 **10개의 Prisma 호출**이 있으며, 데이터베이스 타입 카테고리 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ------------------------------------------------------ |
| 파일 위치 | `backend-node/src/services/dbTypeCategoryService.ts` |
| 파일 크기 | 320+ 라인 |
| Prisma 호출 | 10개 |
| **현재 진행률** | **0/10 (0%)** 🔄 **진행 예정** |
| 복잡도 | 중간 (CRUD, 통계, UPSERT) |
| 우선순위 | 🟡 중간 (Phase 3.8) |
| **상태** | ⏳ **대기 중** |
### 🎯 전환 목표
- ⏳ **10개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ ApiResponse 래퍼 패턴 유지
- ⏳ GROUP BY 통계 쿼리 전환
- ⏳ UPSERT 로직 전환 (ON CONFLICT)
- ⏳ 모든 단위 테스트 통과
- ⏳ **Prisma import 완전 제거**
---
## 🔍 Prisma 사용 현황 분석
### 주요 Prisma 호출 (10개)
#### 1. **getAllCategories()** - 카테고리 목록 조회
```typescript
// Line 45
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
```
#### 2. **getCategoryByTypeCode()** - 카테고리 단건 조회
```typescript
// Line 73
const category = await prisma.db_type_categories.findUnique({
where: { type_code: typeCode }
});
```
#### 3. **createCategory()** - 카테고리 생성
```typescript
// Line 105, 116
const existing = await prisma.db_type_categories.findUnique({
where: { type_code: data.type_code }
});
const category = await prisma.db_type_categories.create({
data: {
type_code: data.type_code,
display_name: data.display_name,
icon: data.icon,
color: data.color,
sort_order: data.sort_order ?? 0,
is_active: true,
}
});
```
#### 4. **updateCategory()** - 카테고리 수정
```typescript
// Line 146
const category = await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: updateData
});
```
#### 5. **deleteCategory()** - 카테고리 삭제 (연결 확인)
```typescript
// Line 179, 193
const connectionsCount = await prisma.external_db_connections.count({
where: { db_type: typeCode }
});
await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: { is_active: false }
});
```
#### 6. **getCategoryStatistics()** - 카테고리별 통계
```typescript
// Line 220, 229
const stats = await prisma.external_db_connections.groupBy({
by: ['db_type'],
_count: { id: true }
});
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true }
});
```
#### 7. **syncPredefinedCategories()** - 사전 정의 카테고리 동기화
```typescript
// Line 300
await prisma.db_type_categories.upsert({
where: { type_code: category.type_code },
update: {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
},
create: {
type_code: category.type_code,
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
is_active: true,
},
});
```
---
## 📝 전환 계획
### 1단계: 기본 CRUD 전환 (5개 함수)
**함수 목록**:
- `getAllCategories()` - 목록 조회 (findMany)
- `getCategoryByTypeCode()` - 단건 조회 (findUnique)
- `createCategory()` - 생성 (findUnique + create)
- `updateCategory()` - 수정 (update)
- `deleteCategory()` - 삭제 (count + update - soft delete)
### 2단계: 통계 및 UPSERT 전환 (2개 함수)
**함수 목록**:
- `getCategoryStatistics()` - 통계 (groupBy + findMany)
- `syncPredefinedCategories()` - 동기화 (upsert)
---
## 💻 전환 예시
### 예시 1: 카테고리 목록 조회 (정렬)
```typescript
// 기존 Prisma
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
// 전환 후
import { query } from "../database/db";
const categories = await query<DbTypeCategory>(
`SELECT * FROM db_type_categories
WHERE is_active = $1
ORDER BY sort_order ASC, display_name ASC`,
[true]
);
```
### 예시 2: 카테고리 생성 (중복 확인)
```typescript
// 기존 Prisma
const existing = await prisma.db_type_categories.findUnique({
where: { type_code: data.type_code }
});
if (existing) {
return {
success: false,
message: "이미 존재하는 타입 코드입니다."
};
}
const category = await prisma.db_type_categories.create({
data: {
type_code: data.type_code,
display_name: data.display_name,
icon: data.icon,
color: data.color,
sort_order: data.sort_order ?? 0,
is_active: true,
}
});
// 전환 후
import { query, queryOne } from "../database/db";
const existing = await queryOne<DbTypeCategory>(
`SELECT * FROM db_type_categories WHERE type_code = $1`,
[data.type_code]
);
if (existing) {
return {
success: false,
message: "이미 존재하는 타입 코드입니다."
};
}
const category = await queryOne<DbTypeCategory>(
`INSERT INTO db_type_categories
(type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING *`,
[
data.type_code,
data.display_name,
data.icon || null,
data.color || null,
data.sort_order ?? 0,
true,
]
);
```
### 예시 3: 동적 UPDATE (변경된 필드만)
```typescript
// 기존 Prisma
const updateData: any = {};
if (data.display_name !== undefined) updateData.display_name = data.display_name;
if (data.icon !== undefined) updateData.icon = data.icon;
if (data.color !== undefined) updateData.color = data.color;
if (data.sort_order !== undefined) updateData.sort_order = data.sort_order;
if (data.is_active !== undefined) updateData.is_active = data.is_active;
const category = await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: updateData
});
// 전환 후
const updateFields: string[] = ["updated_at = NOW()"];
const values: any[] = [];
let paramIndex = 1;
if (data.display_name !== undefined) {
updateFields.push(`display_name = $${paramIndex++}`);
values.push(data.display_name);
}
if (data.icon !== undefined) {
updateFields.push(`icon = $${paramIndex++}`);
values.push(data.icon);
}
if (data.color !== undefined) {
updateFields.push(`color = $${paramIndex++}`);
values.push(data.color);
}
if (data.sort_order !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sort_order);
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(data.is_active);
}
const category = await queryOne<DbTypeCategory>(
`UPDATE db_type_categories
SET ${updateFields.join(", ")}
WHERE type_code = $${paramIndex}
RETURNING *`,
[...values, typeCode]
);
```
### 예시 4: 삭제 전 연결 확인
```typescript
// 기존 Prisma
const connectionsCount = await prisma.external_db_connections.count({
where: { db_type: typeCode }
});
if (connectionsCount > 0) {
return {
success: false,
message: `이 카테고리를 사용 중인 연결이 ${connectionsCount}개 있습니다.`
};
}
await prisma.db_type_categories.update({
where: { type_code: typeCode },
data: { is_active: false }
});
// 전환 후
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM external_db_connections WHERE db_type = $1`,
[typeCode]
);
const connectionsCount = parseInt(countResult?.count || "0");
if (connectionsCount > 0) {
return {
success: false,
message: `이 카테고리를 사용 중인 연결이 ${connectionsCount}개 있습니다.`
};
}
await query(
`UPDATE db_type_categories SET is_active = $1, updated_at = NOW() WHERE type_code = $2`,
[false, typeCode]
);
```
### 예시 5: GROUP BY 통계 + JOIN
```typescript
// 기존 Prisma
const stats = await prisma.external_db_connections.groupBy({
by: ['db_type'],
_count: { id: true }
});
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true }
});
// 전환 후
const stats = await query<{
type_code: string;
display_name: string;
connection_count: string;
}>(
`SELECT
c.type_code,
c.display_name,
COUNT(e.id) as connection_count
FROM db_type_categories c
LEFT JOIN external_db_connections e ON c.type_code = e.db_type
WHERE c.is_active = $1
GROUP BY c.type_code, c.display_name
ORDER BY c.sort_order ASC`,
[true]
);
// 결과 포맷팅
const result = stats.map(row => ({
type_code: row.type_code,
display_name: row.display_name,
connection_count: parseInt(row.connection_count),
}));
```
### 예시 6: UPSERT (ON CONFLICT)
```typescript
// 기존 Prisma
await prisma.db_type_categories.upsert({
where: { type_code: category.type_code },
update: {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
},
create: {
type_code: category.type_code,
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
is_active: true,
},
});
// 전환 후
await query(
`INSERT INTO db_type_categories
(type_code, display_name, icon, color, sort_order, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
ON CONFLICT (type_code)
DO UPDATE SET
display_name = EXCLUDED.display_name,
icon = EXCLUDED.icon,
color = EXCLUDED.color,
sort_order = EXCLUDED.sort_order,
updated_at = NOW()`,
[
category.type_code,
category.display_name,
category.icon || null,
category.color || null,
category.sort_order || 0,
true,
]
);
```
---
## ✅ 완료 기준
- [ ] **10개 모든 Prisma 호출을 Raw Query로 전환 완료**
- [ ] **동적 UPDATE 쿼리 생성**
- [ ] **GROUP BY + LEFT JOIN 통계 쿼리**
- [ ] **ON CONFLICT를 사용한 UPSERT**
- [ ] **ApiResponse 래퍼 패턴 유지**
- [ ] **모든 TypeScript 컴파일 오류 해결**
- [ ] **`import prisma` 완전 제거**
- [ ] **모든 단위 테스트 통과 (10개)**
- [ ] **통합 테스트 작성 완료 (3개 시나리오)**
---
## 🔧 주요 기술적 과제
### 1. ApiResponse 래퍼 패턴
모든 함수가 `ApiResponse<T>` 타입을 반환하므로, 에러 처리를 try-catch로 감싸고 일관된 응답 형식을 유지해야 합니다.
### 2. Soft Delete 패턴
`deleteCategory()`는 실제 DELETE가 아닌 `is_active = false` 업데이트로 처리됩니다.
### 3. 연결 확인
카테고리 삭제 전 `external_db_connections` 테이블에서 사용 중인지 확인해야 합니다.
### 4. UPSERT 로직
PostgreSQL의 `ON CONFLICT` 절을 사용하여 Prisma의 `upsert` 기능을 구현합니다.
### 5. 통계 쿼리 최적화
`groupBy` + 별도 조회 대신, 하나의 `LEFT JOIN` + `GROUP BY` 쿼리로 최적화 가능합니다.
---
## 📋 체크리스트
### 코드 전환
- [ ] import 문 수정 (`prisma` → `query, queryOne`)
- [ ] getAllCategories() - findMany → query
- [ ] getCategoryByTypeCode() - findUnique → queryOne
- [ ] createCategory() - findUnique + create → queryOne (중복 확인 + INSERT)
- [ ] updateCategory() - update → queryOne (동적 UPDATE)
- [ ] deleteCategory() - count + update → queryOne + query
- [ ] getCategoryStatistics() - groupBy + findMany → query (LEFT JOIN)
- [ ] syncPredefinedCategories() - upsert → query (ON CONFLICT)
- [ ] ApiResponse 래퍼 유지
- [ ] Prisma import 완전 제거
### 테스트
- [ ] 단위 테스트 작성 (10개)
- [ ] 통합 테스트 작성 (3개)
- [ ] TypeScript 컴파일 성공
- [ ] 성능 벤치마크 테스트
---
## 💡 특이사항
### ApiResponse 패턴
이 서비스는 모든 메서드가 `ApiResponse<T>` 형식으로 응답을 반환합니다. Raw Query 전환 후에도 이 패턴을 유지해야 합니다.
### 사전 정의 카테고리
`syncPredefinedCategories()` 메서드는 시스템 초기화 시 사전 정의된 DB 타입 카테고리를 동기화합니다. UPSERT 로직이 필수입니다.
### 외래 키 확인
카테고리 삭제 시 `external_db_connections` 테이블에서 사용 중인지 확인하여 데이터 무결성을 보장합니다.
---
**작성일**: 2025-10-01
**예상 소요 시간**: 1시간
**담당자**: 백엔드 개발팀
**우선순위**: 🟡 중간 (Phase 3.8)
**상태**: ⏳ **대기 중**
**특이사항**: ApiResponse 래퍼, UPSERT, GROUP BY + LEFT JOIN 포함

View File

@ -1,391 +0,0 @@
# 📋 Phase 3.9: TemplateStandardService Raw Query 전환 계획
## 📋 개요
TemplateStandardService는 **6개의 Prisma 호출**이 있으며, 템플릿 표준 관리를 담당하는 서비스입니다.
### 📊 기본 정보
| 항목 | 내용 |
| --------------- | ----------------------------------------------------------- |
| 파일 위치 | `backend-node/src/services/templateStandardService.ts` |
| 파일 크기 | 395 라인 |
| Prisma 호출 | 6개 |
| **현재 진행률** | **0/6 (0%)** 🔄 **진행 예정** |
| 복잡도 | 낮음 (기본 CRUD + DISTINCT) |
| 우선순위 | 🟢 낮음 (Phase 3.9) |
| **상태** | ⏳ **대기 중** |
### 🎯 전환 목표
- ⏳ **6개 모든 Prisma 호출을 `db.ts``query()`, `queryOne()` 함수로 교체**
- ⏳ 템플릿 CRUD 기능 정상 동작
- ⏳ DISTINCT 쿼리 전환
- ⏳ 모든 단위 테스트 통과
- ⏳ **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 쿼리 포함

View File

@ -28,10 +28,9 @@ backend-node/src/services/
├── screenManagementService.ts # 화면 관리 (46개 호출) ⭐ 최우선
├── tableManagementService.ts # 테이블 관리 (35개 호출) ⭐ 최우선
├── dataflowService.ts # 데이터플로우 (0개 호출) ✅ 전환 완료
├── dynamicFormService.ts # 동적 폼 (0개 호출) ✅ 전환 완료
├── externalDbConnectionService.ts # 외부DB (0개 호출) ✅ 전환 완료
├── dataflowControlService.ts # 제어관리 (0개 호출) ✅ 전환 완료
├── multilangService.ts # 다국어 (0개 호출) ✅ 전환 완료
├── dynamicFormService.ts # 동적 폼 (15개 호출)
├── externalDbConnectionService.ts # 외부DB (15개 호출)
├── dataflowControlService.ts # 제어관리 (6개 호출)
├── ddlExecutionService.ts # DDL 실행 (6개 호출)
├── authService.ts # 인증 (5개 호출)
└── multiConnectionQueryService.ts # 다중 연결 (4개 호출)
@ -114,21 +113,21 @@ backend-node/ (루트)
- `screenManagementService.ts` (46개) - 화면 정의 관리, JSON 처리
- `tableManagementService.ts` (35개) - 테이블 메타데이터 관리, DDL 실행
- `dataflowService.ts` (0개) - ✅ **전환 완료** (Phase 2.3)
- `dynamicFormService.ts` (0개) - ✅ **전환 완료** (Phase 2.4)
- `externalDbConnectionService.ts` (0개) - ✅ **전환 완료** (Phase 2.5)
- `dataflowControlService.ts` (0개) - ✅ **전환 완료** (Phase 2.6)
- `dynamicFormService.ts` (15개) - UPSERT 및 동적 테이블 처리
- `externalDbConnectionService.ts` (15개) - 외부 DB 연결 관리
- `dataflowControlService.ts` (6개) - 복잡한 제어 로직
- `enhancedDataflowControlService.ts` (0개) - 다중 연결 제어 (Raw Query만 사용)
- `multiConnectionQueryService.ts` (4개) - 외부 DB 연결
#### 🟠 **복잡 (Raw Query 혼재) - 2순위**
- `multilangService.ts` (0개) - ✅ **전환 완료** (Phase 3.1)
- `batchService.ts` (0개) - ✅ **전환 완료** (Phase 3.2)
- `componentStandardService.ts` (0개) - ✅ **전환 완료** (Phase 3.3)
- `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4)
- `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5)
- `collectionService.ts` (0개) - ✅ **전환 완료** (Phase 3.6)
- `layoutService.ts` (0개) - ✅ **전환 완료** (Phase 3.7)
- `multilangService.ts` (25개) - 재귀 쿼리, 다국어 처리
- `batchService.ts` (16개) - 배치 작업 관리
- `componentStandardService.ts` (16개) - 컴포넌트 표준 관리
- `commonCodeService.ts` (15개) - 코드 관리, 계층 구조
- `dataflowDiagramService.ts` (12개) - 다이어그램 관리 ⭐ 신규 발견
- `collectionService.ts` (11개) - 컬렉션 관리
- `layoutService.ts` (10개) - 레이아웃 관리
- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견
- `templateStandardService.ts` (9개) - 템플릿 표준
- `eventTriggerService.ts` (6개) - JSON 검색 쿼리
@ -1100,26 +1099,15 @@ describe("Performance Benchmarks", () => {
#### ⏳ 진행 예정 서비스
- [x] **DynamicFormService 전환 (13개)****완료** (Phase 2.4)
- [x] 13개 Prisma 호출 전환 완료 (동적 폼 CRUD + UPSERT)
- [x] 동적 UPSERT 쿼리 구현 (ON CONFLICT 구문)
- [x] 부분 업데이트 및 타입 변환 로직 유지
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [ ] **DynamicFormService 전환 (13개)** - Phase 2.4 🟢 낮은 우선순위
- 13개 Prisma 호출 ($queryRaw 11개 + ORM 2개)
- SQL은 85% 작성 완료 → `query()` 함수로 교체만 필요
- 📄 **[PHASE2.4_DYNAMIC_FORM_MIGRATION.md](PHASE2.4_DYNAMIC_FORM_MIGRATION.md)**
- [x] **ExternalDbConnectionService 전환 (15개)****완료** (Phase 2.5)
- [x] 15개 Prisma 호출 전환 완료 (외부 DB 연결 CRUD + 테스트)
- [x] 동적 WHERE 조건 생성 및 동적 UPDATE 쿼리 구현
- [x] 암호화/복호화 로직 유지
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.6 🟡 중간 우선순위
- 15개 Prisma 호출 (외부 DB 연결 관리)
- 📄 **[PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md](PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md)**
- [x] **DataflowControlService 전환 (6개)****완료** (Phase 2.6)
- [x] 6개 Prisma 호출 전환 완료 (데이터플로우 제어 + 동적 테이블 CRUD)
- [x] 파라미터 바인딩 수정 (MySQL → PostgreSQL 스타일)
- [x] 복잡한 비즈니스 로직 유지
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [ ] **DataflowControlService 전환 (6개)** - Phase 2.7 🟡 중간 우선순위
- 6개 Prisma 호출 (복잡한 비즈니스 로직)
- 📄 **[PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md](PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md)**
#### ✅ 다른 Phase로 이동
@ -1129,82 +1117,19 @@ describe("Performance Benchmarks", () => {
### **Phase 3: 관리 기능 (2.5주) - 162개 호출**
- [x] **MultiLangService 전환 (25개)****완료** (Phase 3.1)
- [x] 25개 Prisma 호출 전환 완료 (다국어 관리 CRUD)
- [x] 동적 WHERE 조건 및 동적 UPDATE 쿼리 구현
- [x] 트랜잭션 처리 (삭제 + 삽입)
- [x] JOIN 쿼리 (multi_lang_text + multi_lang_key_master)
- [x] IN 절 동적 파라미터 바인딩
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **BatchService 전환 (14개)****완료** (Phase 3.2)
- [x] 14개 Prisma 호출 전환 완료 (배치 설정 CRUD)
- [x] 동적 WHERE 조건 생성 (ILIKE 검색, 페이지네이션)
- [x] 동적 UPDATE 쿼리 (변경된 필드만 업데이트)
- [x] 복잡한 트랜잭션 (배치 설정 + 매핑 동시 생성/수정/삭제)
- [x] LEFT JOIN으로 배치 매핑 조회 (json_agg, COALESCE)
- [x] transaction 함수 활용 (client.query().rows 처리)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **ComponentStandardService 전환 (15개)****완료** (Phase 3.3)
- [x] 15개 Prisma 호출 전환 완료 (컴포넌트 표준 CRUD)
- [x] 동적 WHERE 조건 생성 (ILIKE 검색, OR 조건)
- [x] 동적 UPDATE 쿼리 (fieldMapping 사용)
- [x] GROUP BY 집계 쿼리 (카테고리별, 상태별)
- [x] DISTINCT 쿼리 (카테고리 목록)
- [x] 트랜잭션 처리 (정렬 순서 업데이트)
- [x] SQL 인젝션 방지 (정렬 컬럼 검증)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **CommonCodeService 전환 (10개)****완료** (Phase 3.4)
- [x] 10개 Prisma 호출 전환 완료 (코드 카테고리 및 코드 CRUD)
- [x] 동적 WHERE 조건 생성 (ILIKE 검색, OR 조건)
- [x] 동적 UPDATE 쿼리 (변경된 필드만 업데이트)
- [x] IN 절 동적 파라미터 바인딩 (reorderCodes)
- [x] 트랜잭션 처리 (순서 변경)
- [x] 동적 SQL 쿼리 생성 (중복 검사)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **DataflowDiagramService 전환 (12개)****완료** (Phase 3.5)
- [x] 12개 Prisma 호출 전환 완료 (관계도 CRUD, 복제)
- [x] 동적 WHERE 조건 생성 (company_code 필터링)
- [x] 동적 UPDATE 쿼리 (JSON 필드 포함)
- [x] JSON 필드 처리 (relationships, node_positions, control, category, plan)
- [x] LIKE 검색 (복제 시 이름 패턴 검색)
- [x] 복잡한 복제 로직 (이름 번호 증가)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **CollectionService 전환 (11개)****완료** (Phase 3.6)
- [x] 11개 Prisma 호출 전환 완료 (수집 설정 CRUD, 작업 관리)
- [x] 동적 WHERE 조건 생성 (ILIKE 검색, OR 조건)
- [x] 동적 UPDATE 쿼리 (변경된 필드만 업데이트)
- [x] JSON 필드 처리 (collection_options)
- [x] LEFT JOIN (작업 목록 조회 시 설정 정보 포함)
- [x] 비동기 작업 처리 (setTimeout 내 query 사용)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [x] **LayoutService 전환 (10개)****완료** (Phase 3.7)
- [x] 10개 Prisma 호출 전환 완료 (레이아웃 CRUD, 통계)
- [x] 복잡한 OR 조건 처리 (company_code OR is_public)
- [x] 동적 WHERE 조건 생성 (ILIKE 다중 검색)
- [x] 동적 UPDATE 쿼리 (10개 필드 조건부 업데이트)
- [x] JSON 필드 처리 (default_size, layout_config, zones_config)
- [x] GROUP BY 통계 쿼리 (카테고리별 개수)
- [x] LIKE 검색 (코드 생성 시 패턴 검색)
- [x] Promise.all 병렬 쿼리 (목록 + 개수)
- [x] TypeScript 컴파일 성공
- [x] Prisma import 완전 제거
- [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견
- [ ] BatchExternalDbService (8개)
- [ ] MultiLangService 전환 (25개) - 재귀 쿼리
- [ ] 배치 관련 서비스 전환 (40개) ⭐ 대규모 신규 발견
- [ ] BatchService (16개), BatchExternalDbService (8개)
- [ ] BatchExecutionLogService (7개), BatchManagementService (5개)
- [ ] BatchSchedulerService (4개)
- [ ] 표준 관리 서비스 전환 (6개)
- [ ] TemplateStandardService (6개) - [계획서](PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md)
- [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견
- [ ] DataflowControlService (6개)
- [ ] 기타 중요 서비스 (18개) ⭐ 신규 발견
- [ ] DbTypeCategoryService (10개) - [계획서](PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md)
- [ ] DDLAuditLogger (8개)
- [ ] 표준 관리 서비스 전환 (41개)
- [ ] ComponentStandardService (16개), CommonCodeService (15개)
- [ ] LayoutService (10개)
- [ ] 데이터플로우 관련 서비스 (18개) ⭐ 신규 발견
- [ ] DataflowDiagramService (12개), DataflowControlService (6개)
- [ ] 기타 중요 서비스 (38개) ⭐ 신규 발견
- [ ] CollectionService (11개), DbTypeCategoryService (10개)
- [ ] TemplateStandardService (9개), DDLAuditLogger (8개)
- [ ] 기능별 테스트 완료
### **Phase 4: 확장 기능 (2.5주) - 129개 호출 ⭐ 대폭 확장**

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// 수집 관리 서비스
// 작성일: 2024-12-23
import { query, queryOne, transaction } from "../database/db";
import { PrismaClient } from "@prisma/client";
import {
DataCollectionConfig,
CollectionFilter,
@ -9,6 +9,8 @@ import {
CollectionHistory,
} from "../types/collectionManagement";
const prisma = new PrismaClient();
export class CollectionService {
/**
*
@ -16,44 +18,40 @@ export class CollectionService {
static async getCollectionConfigs(
filter: CollectionFilter
): Promise<DataCollectionConfig[]> {
const whereConditions: string[] = ["company_code = $1"];
const values: any[] = [filter.company_code || "*"];
let paramIndex = 2;
const whereCondition: any = {
company_code: filter.company_code || "*",
};
if (filter.config_name) {
whereConditions.push(`config_name ILIKE $${paramIndex++}`);
values.push(`%${filter.config_name}%`);
whereCondition.config_name = {
contains: filter.config_name,
mode: "insensitive",
};
}
if (filter.source_connection_id) {
whereConditions.push(`source_connection_id = $${paramIndex++}`);
values.push(filter.source_connection_id);
whereCondition.source_connection_id = filter.source_connection_id;
}
if (filter.collection_type) {
whereConditions.push(`collection_type = $${paramIndex++}`);
values.push(filter.collection_type);
whereCondition.collection_type = filter.collection_type;
}
if (filter.is_active) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(filter.is_active === "Y");
whereCondition.is_active = filter.is_active === "Y";
}
if (filter.search) {
whereConditions.push(
`(config_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
values.push(`%${filter.search}%`);
paramIndex++;
whereCondition.OR = [
{ config_name: { contains: filter.search, mode: "insensitive" } },
{ description: { contains: filter.search, mode: "insensitive" } },
];
}
const configs = await query<any>(
`SELECT * FROM data_collection_configs
WHERE ${whereConditions.join(" AND ")}
ORDER BY created_date DESC`,
values
);
const configs = await prisma.data_collection_configs.findMany({
where: whereCondition,
orderBy: { created_date: "desc" },
});
return configs.map((config: any) => ({
...config,
@ -67,10 +65,9 @@ export class CollectionService {
static async getCollectionConfigById(
id: number
): Promise<DataCollectionConfig | null> {
const config = await queryOne<any>(
`SELECT * FROM data_collection_configs WHERE id = $1`,
[id]
);
const config = await prisma.data_collection_configs.findUnique({
where: { id },
});
if (!config) return null;
@ -87,26 +84,15 @@ export class CollectionService {
data: DataCollectionConfig
): Promise<DataCollectionConfig> {
const { id, collection_options, ...createData } = data;
const config = await queryOne<any>(
`INSERT INTO data_collection_configs
(config_name, company_code, source_connection_id, collection_type,
collection_options, schedule_cron, is_active, description,
created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
RETURNING *`,
[
createData.config_name,
createData.company_code,
createData.source_connection_id,
createData.collection_type,
collection_options ? JSON.stringify(collection_options) : null,
createData.schedule_cron,
data.is_active,
createData.description,
createData.created_by,
createData.updated_by,
]
);
const config = await prisma.data_collection_configs.create({
data: {
...createData,
is_active: data.is_active,
collection_options: collection_options || undefined,
created_date: new Date(),
updated_date: new Date(),
},
});
return {
...config,
@ -121,52 +107,19 @@ export class CollectionService {
id: number,
data: Partial<DataCollectionConfig>
): Promise<DataCollectionConfig> {
const updateFields: string[] = ["updated_date = NOW()"];
const values: any[] = [];
let paramIndex = 1;
const updateData: any = {
...data,
updated_date: new Date(),
};
if (data.config_name !== undefined) {
updateFields.push(`config_name = $${paramIndex++}`);
values.push(data.config_name);
}
if (data.source_connection_id !== undefined) {
updateFields.push(`source_connection_id = $${paramIndex++}`);
values.push(data.source_connection_id);
}
if (data.collection_type !== undefined) {
updateFields.push(`collection_type = $${paramIndex++}`);
values.push(data.collection_type);
}
if (data.collection_options !== undefined) {
updateFields.push(`collection_options = $${paramIndex++}`);
values.push(
data.collection_options ? JSON.stringify(data.collection_options) : null
);
}
if (data.schedule_cron !== undefined) {
updateFields.push(`schedule_cron = $${paramIndex++}`);
values.push(data.schedule_cron);
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(data.is_active);
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.updated_by !== undefined) {
updateFields.push(`updated_by = $${paramIndex++}`);
values.push(data.updated_by);
updateData.is_active = data.is_active;
}
const config = await queryOne<any>(
`UPDATE data_collection_configs
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
RETURNING *`,
[...values, id]
);
const config = await prisma.data_collection_configs.update({
where: { id },
data: updateData,
});
return {
...config,
@ -178,17 +131,18 @@ export class CollectionService {
*
*/
static async deleteCollectionConfig(id: number): Promise<void> {
await query(`DELETE FROM data_collection_configs WHERE id = $1`, [id]);
await prisma.data_collection_configs.delete({
where: { id },
});
}
/**
*
*/
static async executeCollection(configId: number): Promise<CollectionJob> {
const config = await queryOne<any>(
`SELECT * FROM data_collection_configs WHERE id = $1`,
[configId]
);
const config = await prisma.data_collection_configs.findUnique({
where: { id: configId },
});
if (!config) {
throw new Error("수집 설정을 찾을 수 없습니다.");
@ -199,13 +153,14 @@ export class CollectionService {
}
// 수집 작업 기록 생성
const job = await queryOne<any>(
`INSERT INTO data_collection_jobs
(config_id, job_status, started_at, created_date)
VALUES ($1, $2, NOW(), NOW())
RETURNING *`,
[configId, "running"]
);
const job = await prisma.data_collection_jobs.create({
data: {
config_id: configId,
job_status: "running",
started_at: new Date(),
created_date: new Date(),
},
});
// 실제 수집 작업 실행 로직은 여기에 구현
// 현재는 시뮬레이션으로 처리
@ -216,23 +171,24 @@ export class CollectionService {
const recordsCollected = Math.floor(Math.random() * 1000) + 100;
await query(
`UPDATE data_collection_jobs
SET job_status = $1, completed_at = NOW(), records_processed = $2
WHERE id = $3`,
["completed", recordsCollected, job.id]
);
await prisma.data_collection_jobs.update({
where: { id: job.id },
data: {
job_status: "completed",
completed_at: new Date(),
records_processed: recordsCollected,
},
});
} catch (error) {
await query(
`UPDATE data_collection_jobs
SET job_status = $1, completed_at = NOW(), error_message = $2
WHERE id = $3`,
[
"failed",
error instanceof Error ? error.message : "알 수 없는 오류",
job.id,
]
);
await prisma.data_collection_jobs.update({
where: { id: job.id },
data: {
job_status: "failed",
completed_at: new Date(),
error_message:
error instanceof Error ? error.message : "알 수 없는 오류",
},
});
}
}, 0);
@ -243,21 +199,24 @@ export class CollectionService {
*
*/
static async getCollectionJobs(configId?: number): Promise<CollectionJob[]> {
let sql = `
SELECT j.*, c.config_name, c.collection_type
FROM data_collection_jobs j
LEFT JOIN data_collection_configs c ON j.config_id = c.id
`;
const values: any[] = [];
const whereCondition: any = {};
if (configId) {
sql += ` WHERE j.config_id = $1`;
values.push(configId);
whereCondition.config_id = configId;
}
sql += ` ORDER BY j.started_at DESC`;
const jobs = await query<any>(sql, values);
const jobs = await prisma.data_collection_jobs.findMany({
where: whereCondition,
orderBy: { started_at: "desc" },
include: {
config: {
select: {
config_name: true,
collection_type: true,
},
},
},
});
return jobs as CollectionJob[];
}
@ -268,13 +227,11 @@ export class CollectionService {
static async getCollectionHistory(
configId: number
): Promise<CollectionHistory[]> {
const history = await query<any>(
`SELECT * FROM data_collection_jobs
WHERE config_id = $1
ORDER BY started_at DESC
LIMIT 50`,
[configId]
);
const history = await prisma.data_collection_jobs.findMany({
where: { config_id: configId },
orderBy: { started_at: "desc" },
take: 50, // 최근 50개 이력
});
return history.map((item: any) => ({
id: item.id,

View File

@ -1,4 +1,5 @@
import { query, queryOne, transaction } from "../database/db";
import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import { logger } from "../utils/logger";
export interface CodeCategory {
@ -68,46 +69,30 @@ export class CommonCodeService {
try {
const { search, isActive, page = 1, size = 20 } = params;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
let whereClause: any = {};
if (search) {
whereConditions.push(
`(category_name ILIKE $${paramIndex} OR category_code ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
whereClause.OR = [
{ category_name: { contains: search, mode: "insensitive" } },
{ category_code: { contains: search, mode: "insensitive" } },
];
}
if (isActive !== undefined) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(isActive ? "Y" : "N");
whereClause.is_active = isActive ? "Y" : "N";
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const offset = (page - 1) * size;
// 카테고리 조회
const categories = await query<CodeCategory>(
`SELECT * FROM code_category
${whereClause}
ORDER BY sort_order ASC, category_code ASC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, offset]
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM code_category ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
const [categories, total] = await Promise.all([
prisma.code_category.findMany({
where: whereClause,
orderBy: [{ sort_order: "asc" }, { category_code: "asc" }],
skip: offset,
take: size,
}),
prisma.code_category.count({ where: whereClause }),
]);
logger.info(
`카테고리 조회 완료: ${categories.length}개, 전체: ${total}`
@ -130,43 +115,32 @@ export class CommonCodeService {
try {
const { search, isActive, page = 1, size = 20 } = params;
const whereConditions: string[] = ["code_category = $1"];
const values: any[] = [categoryCode];
let paramIndex = 2;
let whereClause: any = {
code_category: categoryCode,
};
if (search) {
whereConditions.push(
`(code_name ILIKE $${paramIndex} OR code_value ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
whereClause.OR = [
{ code_name: { contains: search, mode: "insensitive" } },
{ code_value: { contains: search, mode: "insensitive" } },
];
}
if (isActive !== undefined) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(isActive ? "Y" : "N");
whereClause.is_active = isActive ? "Y" : "N";
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const offset = (page - 1) * size;
// 코드 조회
const codes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY sort_order ASC, code_value ASC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, offset]
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM code_info ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
const [codes, total] = await Promise.all([
prisma.code_info.findMany({
where: whereClause,
orderBy: [{ sort_order: "asc" }, { code_value: "asc" }],
skip: offset,
take: size,
}),
prisma.code_info.count({ where: whereClause }),
]);
logger.info(
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}`
@ -184,22 +158,18 @@ export class CommonCodeService {
*/
async createCategory(data: CreateCategoryData, createdBy: string) {
try {
const category = await queryOne<CodeCategory>(
`INSERT INTO code_category
(category_code, category_name, category_name_eng, description, sort_order,
is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, NOW(), NOW())
RETURNING *`,
[
data.categoryCode,
data.categoryName,
data.categoryNameEng || null,
data.description || null,
data.sortOrder || 0,
createdBy,
createdBy,
]
);
const category = await prisma.code_category.create({
data: {
category_code: data.categoryCode,
category_name: data.categoryName,
category_name_eng: data.categoryNameEng,
description: data.description,
sort_order: data.sortOrder || 0,
is_active: "Y",
created_by: createdBy,
updated_by: createdBy,
},
});
logger.info(`카테고리 생성 완료: ${data.categoryCode}`);
return category;
@ -220,49 +190,23 @@ export class CommonCodeService {
try {
// 디버깅: 받은 데이터 로그
logger.info(`카테고리 수정 데이터:`, { categoryCode, data });
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = [
"updated_by = $1",
"updated_date = NOW()",
];
const values: any[] = [updatedBy];
let paramIndex = 2;
if (data.categoryName !== undefined) {
updateFields.push(`category_name = $${paramIndex++}`);
values.push(data.categoryName);
}
if (data.categoryNameEng !== undefined) {
updateFields.push(`category_name_eng = $${paramIndex++}`);
values.push(data.categoryNameEng);
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.sortOrder !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sortOrder);
}
if (data.isActive !== undefined) {
const activeValue =
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive;
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
const category = await queryOne<CodeCategory>(
`UPDATE code_category
SET ${updateFields.join(", ")}
WHERE category_code = $${paramIndex}
RETURNING *`,
[...values, categoryCode]
);
const category = await prisma.code_category.update({
where: { category_code: categoryCode },
data: {
category_name: data.categoryName,
category_name_eng: data.categoryNameEng,
description: data.description,
sort_order: data.sortOrder,
is_active:
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive, // boolean이면 "Y"/"N"으로 변환
updated_by: updatedBy,
updated_date: new Date(),
},
});
logger.info(`카테고리 수정 완료: ${categoryCode}`);
return category;
@ -277,9 +221,9 @@ export class CommonCodeService {
*/
async deleteCategory(categoryCode: string) {
try {
await query(`DELETE FROM code_category WHERE category_code = $1`, [
categoryCode,
]);
await prisma.code_category.delete({
where: { category_code: categoryCode },
});
logger.info(`카테고리 삭제 완료: ${categoryCode}`);
} catch (error) {
@ -297,23 +241,19 @@ export class CommonCodeService {
createdBy: string
) {
try {
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, NOW(), NOW())
RETURNING *`,
[
categoryCode,
data.codeValue,
data.codeName,
data.codeNameEng || null,
data.description || null,
data.sortOrder || 0,
createdBy,
createdBy,
]
);
const code = await prisma.code_info.create({
data: {
code_category: categoryCode,
code_value: data.codeValue,
code_name: data.codeName,
code_name_eng: data.codeNameEng,
description: data.description,
sort_order: data.sortOrder || 0,
is_active: "Y",
created_by: createdBy,
updated_by: createdBy,
},
});
logger.info(`코드 생성 완료: ${categoryCode}.${data.codeValue}`);
return code;
@ -338,49 +278,28 @@ export class CommonCodeService {
try {
// 디버깅: 받은 데이터 로그
logger.info(`코드 수정 데이터:`, { categoryCode, codeValue, data });
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = [
"updated_by = $1",
"updated_date = NOW()",
];
const values: any[] = [updatedBy];
let paramIndex = 2;
if (data.codeName !== undefined) {
updateFields.push(`code_name = $${paramIndex++}`);
values.push(data.codeName);
}
if (data.codeNameEng !== undefined) {
updateFields.push(`code_name_eng = $${paramIndex++}`);
values.push(data.codeNameEng);
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.sortOrder !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sortOrder);
}
if (data.isActive !== undefined) {
const activeValue =
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive;
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
const code = await queryOne<CodeInfo>(
`UPDATE code_info
SET ${updateFields.join(", ")}
WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}
RETURNING *`,
[...values, categoryCode, codeValue]
);
const code = await prisma.code_info.update({
where: {
code_category_code_value: {
code_category: categoryCode,
code_value: codeValue,
},
},
data: {
code_name: data.codeName,
code_name_eng: data.codeNameEng,
description: data.description,
sort_order: data.sortOrder,
is_active:
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive, // boolean이면 "Y"/"N"으로 변환
updated_by: updatedBy,
updated_date: new Date(),
},
});
logger.info(`코드 수정 완료: ${categoryCode}.${codeValue}`);
return code;
@ -395,10 +314,14 @@ export class CommonCodeService {
*/
async deleteCode(categoryCode: string, codeValue: string) {
try {
await query(
`DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, codeValue]
);
await prisma.code_info.delete({
where: {
code_category_code_value: {
code_category: categoryCode,
code_value: codeValue,
},
},
});
logger.info(`코드 삭제 완료: ${categoryCode}.${codeValue}`);
} catch (error) {
@ -412,18 +335,19 @@ export class CommonCodeService {
*/
async getCodeOptions(categoryCode: string) {
try {
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
sort_order: number;
}>(
`SELECT code_value, code_name, code_name_eng, sort_order
FROM code_info
WHERE code_category = $1 AND is_active = 'Y'
ORDER BY sort_order ASC, code_value ASC`,
[categoryCode]
);
const codes = await prisma.code_info.findMany({
where: {
code_category: categoryCode,
is_active: "Y",
},
select: {
code_value: true,
code_name: true,
code_name_eng: true,
sort_order: true,
},
orderBy: [{ sort_order: "asc" }, { code_value: "asc" }],
});
const options = codes.map((code) => ({
value: code.code_value,
@ -449,14 +373,13 @@ export class CommonCodeService {
) {
try {
// 먼저 존재하는 코드들을 확인
const codeValues = codes.map((c) => c.codeValue);
const placeholders = codeValues.map((_, i) => `$${i + 2}`).join(", ");
const existingCodes = await query<{ code_value: string }>(
`SELECT code_value FROM code_info
WHERE code_category = $1 AND code_value IN (${placeholders})`,
[categoryCode, ...codeValues]
);
const existingCodes = await prisma.code_info.findMany({
where: {
code_category: categoryCode,
code_value: { in: codes.map((c) => c.codeValue) },
},
select: { code_value: true },
});
const existingCodeValues = existingCodes.map((c) => c.code_value);
const validCodes = codes.filter((c) =>
@ -469,17 +392,23 @@ export class CommonCodeService {
);
}
// 트랜잭션으로 업데이트
await transaction(async (client) => {
for (const { codeValue, sortOrder } of validCodes) {
await client.query(
`UPDATE code_info
SET sort_order = $1, updated_by = $2, updated_date = NOW()
WHERE code_category = $3 AND code_value = $4`,
[sortOrder, updatedBy, categoryCode, codeValue]
);
}
});
const updatePromises = validCodes.map(({ codeValue, sortOrder }) =>
prisma.code_info.update({
where: {
code_category_code_value: {
code_category: categoryCode,
code_value: codeValue,
},
},
data: {
sort_order: sortOrder,
updated_by: updatedBy,
updated_date: new Date(),
},
})
);
await Promise.all(updatePromises);
const skippedCodes = codes.filter(
(c) => !existingCodeValues.includes(c.codeValue)
@ -531,38 +460,18 @@ export class CommonCodeService {
break;
}
// SQL 쿼리 생성
let sql = "";
const values: any[] = [];
let paramIndex = 1;
switch (field) {
case "categoryCode":
sql = `SELECT category_code FROM code_category WHERE category_code = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "categoryName":
sql = `SELECT category_code FROM code_category WHERE category_name = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "categoryNameEng":
sql = `SELECT category_code FROM code_category WHERE category_name_eng = $${paramIndex++}`;
values.push(trimmedValue);
break;
}
// 수정 시 자기 자신 제외
if (excludeCategoryCode) {
sql += ` AND category_code != $${paramIndex++}`;
values.push(excludeCategoryCode);
whereCondition.category_code = {
...whereCondition.category_code,
not: excludeCategoryCode,
};
}
sql += ` LIMIT 1`;
const existingCategory = await queryOne<{ category_code: string }>(
sql,
values
);
const existingCategory = await prisma.code_category.findFirst({
where: whereCondition,
select: { category_code: true },
});
const isDuplicate = !!existingCategory;
const fieldNames = {
@ -618,36 +527,18 @@ export class CommonCodeService {
break;
}
// SQL 쿼리 생성
let sql =
"SELECT code_value FROM code_info WHERE code_category = $1 AND ";
const values: any[] = [categoryCode];
let paramIndex = 2;
switch (field) {
case "codeValue":
sql += `code_value = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "codeName":
sql += `code_name = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "codeNameEng":
sql += `code_name_eng = $${paramIndex++}`;
values.push(trimmedValue);
break;
}
// 수정 시 자기 자신 제외
if (excludeCodeValue) {
sql += ` AND code_value != $${paramIndex++}`;
values.push(excludeCodeValue);
whereCondition.code_value = {
...whereCondition.code_value,
not: excludeCodeValue,
};
}
sql += ` LIMIT 1`;
const existingCode = await queryOne<{ code_value: string }>(sql, values);
const existingCode = await prisma.code_info.findFirst({
where: whereCondition,
select: { code_value: true },
});
const isDuplicate = !!existingCode;
const fieldNames = {

View File

@ -1,4 +1,6 @@
import { query, queryOne, transaction } from "../database/db";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export interface ComponentStandardData {
component_code: string;
@ -47,78 +49,49 @@ class ComponentStandardService {
offset = 0,
} = params;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
const where: any = {};
// 활성화 상태 필터
if (active) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(active);
where.is_active = active;
}
// 카테고리 필터
if (category && category !== "all") {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
where.category = category;
}
// 공개 여부 필터
if (is_public) {
whereConditions.push(`is_public = $${paramIndex++}`);
values.push(is_public);
where.is_public = is_public;
}
// 회사별 필터 (공개 컴포넌트 + 해당 회사 컴포넌트)
if (company_code) {
whereConditions.push(
`(is_public = 'Y' OR company_code = $${paramIndex++})`
);
values.push(company_code);
where.OR = [{ is_public: "Y" }, { company_code }];
}
// 검색 조건
if (search) {
whereConditions.push(
`(component_name ILIKE $${paramIndex} OR component_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
where.OR = [
...(where.OR || []),
{ component_name: { contains: search, mode: "insensitive" } },
{ component_name_eng: { contains: search, mode: "insensitive" } },
{ description: { contains: search, mode: "insensitive" } },
];
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const orderBy: any = {};
orderBy[sort] = order;
// 정렬 컬럼 검증 (SQL 인젝션 방지)
const validSortColumns = [
"sort_order",
"component_name",
"category",
"created_date",
"updated_date",
];
const sortColumn = validSortColumns.includes(sort) ? sort : "sort_order";
const sortOrder = order === "desc" ? "DESC" : "ASC";
const components = await prisma.component_standards.findMany({
where,
orderBy,
take: limit,
skip: offset,
});
// 컴포넌트 조회
const components = await query<any>(
`SELECT * FROM component_standards
${whereClause}
ORDER BY ${sortColumn} ${sortOrder}
${limit ? `LIMIT $${paramIndex++}` : ""}
${limit ? `OFFSET $${paramIndex++}` : ""}`,
limit ? [...values, limit, offset] : values
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM component_standards ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
const total = await prisma.component_standards.count({ where });
return {
components,
@ -132,10 +105,9 @@ class ComponentStandardService {
*
*/
async getComponent(component_code: string) {
const component = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[component_code]
);
const component = await prisma.component_standards.findUnique({
where: { component_code },
});
if (!component) {
throw new Error(`컴포넌트를 찾을 수 없습니다: ${component_code}`);
@ -149,10 +121,9 @@ class ComponentStandardService {
*/
async createComponent(data: ComponentStandardData) {
// 중복 코드 확인
const existing = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[data.component_code]
);
const existing = await prisma.component_standards.findUnique({
where: { component_code: data.component_code },
});
if (existing) {
throw new Error(
@ -167,31 +138,13 @@ class ComponentStandardService {
delete (createData as any).active;
}
const component = await queryOne<any>(
`INSERT INTO component_standards
(component_code, component_name, component_name_eng, description, category,
icon_name, default_size, component_config, preview_image, sort_order,
is_active, is_public, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
RETURNING *`,
[
createData.component_code,
createData.component_name,
createData.component_name_eng || null,
createData.description || null,
createData.category,
createData.icon_name || null,
createData.default_size || null,
createData.component_config,
createData.preview_image || null,
createData.sort_order || 0,
createData.is_active || "Y",
createData.is_public || "N",
createData.company_code,
createData.created_by || null,
createData.updated_by || null,
]
);
const component = await prisma.component_standards.create({
data: {
...createData,
created_date: new Date(),
updated_date: new Date(),
},
});
return component;
}
@ -212,41 +165,13 @@ class ComponentStandardService {
delete (updateData as any).active;
}
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_date = NOW()"];
const values: any[] = [];
let paramIndex = 1;
const fieldMapping: { [key: string]: string } = {
component_name: "component_name",
component_name_eng: "component_name_eng",
description: "description",
category: "category",
icon_name: "icon_name",
default_size: "default_size",
component_config: "component_config",
preview_image: "preview_image",
sort_order: "sort_order",
is_active: "is_active",
is_public: "is_public",
company_code: "company_code",
updated_by: "updated_by",
};
for (const [key, dbField] of Object.entries(fieldMapping)) {
if (key in updateData) {
updateFields.push(`${dbField} = $${paramIndex++}`);
values.push((updateData as any)[key]);
}
}
const component = await queryOne<any>(
`UPDATE component_standards
SET ${updateFields.join(", ")}
WHERE component_code = $${paramIndex}
RETURNING *`,
[...values, component_code]
);
const component = await prisma.component_standards.update({
where: { component_code },
data: {
...updateData,
updated_date: new Date(),
},
});
return component;
}
@ -257,9 +182,9 @@ class ComponentStandardService {
async deleteComponent(component_code: string) {
const existing = await this.getComponent(component_code);
await query(`DELETE FROM component_standards WHERE component_code = $1`, [
component_code,
]);
await prisma.component_standards.delete({
where: { component_code },
});
return { message: `컴포넌트가 삭제되었습니다: ${component_code}` };
}
@ -270,16 +195,14 @@ class ComponentStandardService {
async updateSortOrder(
updates: Array<{ component_code: string; sort_order: number }>
) {
await transaction(async (client) => {
for (const { component_code, sort_order } of updates) {
await client.query(
`UPDATE component_standards
SET sort_order = $1, updated_date = NOW()
WHERE component_code = $2`,
[sort_order, component_code]
);
}
});
const transactions = updates.map(({ component_code, sort_order }) =>
prisma.component_standards.update({
where: { component_code },
data: { sort_order, updated_date: new Date() },
})
);
await prisma.$transaction(transactions);
return { message: "정렬 순서가 업데이트되었습니다." };
}
@ -295,38 +218,33 @@ class ComponentStandardService {
const source = await this.getComponent(source_code);
// 새 코드 중복 확인
const existing = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[new_code]
);
const existing = await prisma.component_standards.findUnique({
where: { component_code: new_code },
});
if (existing) {
throw new Error(`이미 존재하는 컴포넌트 코드입니다: ${new_code}`);
}
const component = await queryOne<any>(
`INSERT INTO component_standards
(component_code, component_name, component_name_eng, description, category,
icon_name, default_size, component_config, preview_image, sort_order,
is_active, is_public, company_code, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW())
RETURNING *`,
[
new_code,
new_name,
source?.component_name_eng,
source?.description,
source?.category,
source?.icon_name,
source?.default_size,
source?.component_config,
source?.preview_image,
source?.sort_order,
source?.is_active,
source?.is_public,
source?.company_code || "DEFAULT",
]
);
const component = await prisma.component_standards.create({
data: {
component_code: new_code,
component_name: new_name,
component_name_eng: source?.component_name_eng,
description: source?.description,
category: source?.category,
icon_name: source?.icon_name,
default_size: source?.default_size as any,
component_config: source?.component_config as any,
preview_image: source?.preview_image,
sort_order: source?.sort_order,
is_active: source?.is_active,
is_public: source?.is_public,
company_code: source?.company_code || "DEFAULT",
created_date: new Date(),
updated_date: new Date(),
},
});
return component;
}
@ -335,20 +253,19 @@ class ComponentStandardService {
*
*/
async getCategories(company_code?: string) {
const whereConditions: string[] = ["is_active = 'Y'"];
const values: any[] = [];
const where: any = {
is_active: "Y",
};
if (company_code) {
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
values.push(company_code);
where.OR = [{ is_public: "Y" }, { company_code }];
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const categories = await query<{ category: string }>(
`SELECT DISTINCT category FROM component_standards ${whereClause} ORDER BY category`,
values
);
const categories = await prisma.component_standards.findMany({
where,
select: { category: true },
distinct: ["category"],
});
return categories
.map((item) => item.category)
@ -359,48 +276,36 @@ class ComponentStandardService {
*
*/
async getStatistics(company_code?: string) {
const whereConditions: string[] = ["is_active = 'Y'"];
const values: any[] = [];
const where: any = {
is_active: "Y",
};
if (company_code) {
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
values.push(company_code);
where.OR = [{ is_public: "Y" }, { company_code }];
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const total = await prisma.component_standards.count({ where });
// 전체 개수
const totalResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM component_standards ${whereClause}`,
values
);
const total = parseInt(totalResult?.count || "0");
const byCategory = await prisma.component_standards.groupBy({
by: ["category"],
where,
_count: { category: true },
});
// 카테고리별 집계
const byCategory = await query<{ category: string; count: string }>(
`SELECT category, COUNT(*) as count
FROM component_standards
${whereClause}
GROUP BY category`,
values
);
// 상태별 집계
const byStatus = await query<{ is_active: string; count: string }>(
`SELECT is_active, COUNT(*) as count
FROM component_standards
GROUP BY is_active`
);
const byStatus = await prisma.component_standards.groupBy({
by: ["is_active"],
_count: { is_active: true },
});
return {
total,
byCategory: byCategory.map((item) => ({
category: item.category,
count: parseInt(item.count),
count: item._count.category,
})),
byStatus: byStatus.map((item) => ({
status: item.is_active,
count: parseInt(item.count),
count: item._count.is_active,
})),
};
}
@ -412,21 +317,16 @@ class ComponentStandardService {
component_code: string,
company_code?: string
): Promise<boolean> {
const whereConditions: string[] = ["component_code = $1"];
const values: any[] = [component_code];
const whereClause: any = { component_code };
// 회사 코드가 있고 "*"가 아닌 경우에만 조건 추가
if (company_code && company_code !== "*") {
whereConditions.push("company_code = $2");
values.push(company_code);
whereClause.company_code = company_code;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const existingComponent = await queryOne<any>(
`SELECT * FROM component_standards ${whereClause} LIMIT 1`,
values
);
const existingComponent = await prisma.component_standards.findFirst({
where: whereClause,
});
return !!existingComponent;
}

View File

@ -1,4 +1,5 @@
import { query, queryOne } from "../database/db";
// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용
import prisma = require("../config/database");
export interface ControlCondition {
id: string;
@ -81,10 +82,9 @@ export class DataflowControlService {
});
// 관계도 정보 조회
const diagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams WHERE diagram_id = $1`,
[diagramId]
);
const diagram = await prisma.dataflow_diagrams.findUnique({
where: { diagram_id: diagramId },
});
if (!diagram) {
return {
@ -527,9 +527,9 @@ export class DataflowControlService {
}
// 대상 테이블에서 조건에 맞는 데이터 조회
const queryResult = await query<Record<string, any>>(
const queryResult = await prisma.$queryRawUnsafe(
`SELECT ${condition.field} FROM ${tableName} WHERE ${condition.field} = $1 LIMIT 1`,
[condition.value]
condition.value
);
dataToCheck =
@ -758,14 +758,14 @@ export class DataflowControlService {
try {
// 동적 테이블 INSERT 실행
const placeholders = Object.keys(insertData)
.map((_, i) => `$${i + 1}`)
.join(", ");
const result = await query(
`INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")})
VALUES (${placeholders})`,
Object.values(insertData)
const result = await prisma.$executeRawUnsafe(
`
INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")})
VALUES (${Object.keys(insertData)
.map(() => "?")
.join(", ")})
`,
...Object.values(insertData)
);
results.push({
@ -878,7 +878,10 @@ export class DataflowControlService {
);
console.log(`📊 쿼리 파라미터:`, allValues);
const result = await query(updateQuery, allValues);
const result = await prisma.$executeRawUnsafe(
updateQuery,
...allValues
);
console.log(
`✅ UPDATE 성공 (${i + 1}/${action.fieldMappings.length}):`,
@ -1030,7 +1033,10 @@ export class DataflowControlService {
console.log(`🚀 실행할 쿼리:`, deleteQuery);
console.log(`📊 쿼리 파라미터:`, whereValues);
const result = await query(deleteQuery, whereValues);
const result = await prisma.$executeRawUnsafe(
deleteQuery,
...whereValues
);
console.log(`✅ DELETE 성공:`, {
table: tableName,
@ -1083,15 +1089,18 @@ export class DataflowControlService {
columnName: string
): Promise<boolean> {
try {
const result = await query<{ exists: boolean }>(
`SELECT EXISTS (
const result = await prisma.$queryRawUnsafe<Array<{ exists: boolean }>>(
`
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = $1
AND column_name = $2
AND table_schema = 'public'
) as exists`,
[tableName, columnName]
) as exists
`,
tableName,
columnName
);
return result[0]?.exists || false;

View File

@ -1,4 +1,5 @@
import { query, queryOne, transaction } from "../database/db";
import { Prisma } from "@prisma/client";
import prisma from "../config/database";
import { logger } from "../utils/logger";
// 타입 정의
@ -42,41 +43,41 @@ export const getDataflowDiagrams = async (
try {
const offset = (page - 1) * size;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
// 검색 조건 구성
const whereClause: {
company_code?: string;
diagram_name?: {
contains: string;
mode: "insensitive";
};
} = {};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex++}`);
values.push(companyCode);
whereClause.company_code = companyCode;
}
if (searchTerm) {
whereConditions.push(`diagram_name ILIKE $${paramIndex++}`);
values.push(`%${searchTerm}%`);
whereClause.diagram_name = {
contains: searchTerm,
mode: "insensitive",
};
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// 총 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM dataflow_diagrams ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
const total = await prisma.dataflow_diagrams.count({
where: whereClause,
});
// 데이터 조회
const diagrams = await query<any>(
`SELECT * FROM dataflow_diagrams
${whereClause}
ORDER BY updated_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, offset]
);
const diagrams = await prisma.dataflow_diagrams.findMany({
where: whereClause,
orderBy: {
updated_at: "desc",
},
skip: offset,
take: size,
});
const totalPages = Math.ceil(total / size);
@ -103,21 +104,21 @@ export const getDataflowDiagramById = async (
companyCode: string
) => {
try {
const whereConditions: string[] = ["diagram_id = $1"];
const values: any[] = [diagramId];
const whereClause: {
diagram_id: number;
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
whereConditions.push("company_code = $2");
values.push(companyCode);
whereClause.company_code = companyCode;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const diagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams ${whereClause} LIMIT 1`,
values
);
const diagram = await prisma.dataflow_diagrams.findFirst({
where: whereClause,
});
return diagram;
} catch (error) {
@ -133,24 +134,23 @@ export const createDataflowDiagram = async (
data: CreateDataflowDiagramData
) => {
try {
const newDiagram = await queryOne<any>(
`INSERT INTO dataflow_diagrams
(diagram_name, relationships, node_positions, category, control, plan,
company_code, created_by, updated_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
RETURNING *`,
[
data.diagram_name,
JSON.stringify(data.relationships),
data.node_positions ? JSON.stringify(data.node_positions) : null,
data.category ? JSON.stringify(data.category) : null,
data.control ? JSON.stringify(data.control) : null,
data.plan ? JSON.stringify(data.plan) : null,
data.company_code,
data.created_by,
data.updated_by,
]
);
const newDiagram = await prisma.dataflow_diagrams.create({
data: {
diagram_name: data.diagram_name,
relationships: data.relationships as Prisma.InputJsonValue,
node_positions: data.node_positions as
| Prisma.InputJsonValue
| undefined,
category: data.category
? (data.category as Prisma.InputJsonValue)
: undefined,
control: data.control as Prisma.InputJsonValue | undefined,
plan: data.plan as Prisma.InputJsonValue | undefined,
company_code: data.company_code,
created_by: data.created_by,
updated_by: data.updated_by,
},
});
return newDiagram;
} catch (error) {
@ -173,18 +173,21 @@ export const updateDataflowDiagram = async (
);
// 먼저 해당 관계도가 존재하는지 확인
const whereConditions: string[] = ["diagram_id = $1"];
const checkValues: any[] = [diagramId];
const whereClause: {
diagram_id: number;
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
whereConditions.push("company_code = $2");
checkValues.push(companyCode);
whereClause.company_code = companyCode;
}
const existingDiagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
checkValues
);
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
where: whereClause,
});
logger.info(
`기존 관계도 조회 결과:`,
@ -198,45 +201,36 @@ export const updateDataflowDiagram = async (
return null;
}
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_by = $1", "updated_at = NOW()"];
const values: any[] = [data.updated_by];
let paramIndex = 2;
if (data.diagram_name) {
updateFields.push(`diagram_name = $${paramIndex++}`);
values.push(data.diagram_name);
}
if (data.relationships) {
updateFields.push(`relationships = $${paramIndex++}`);
values.push(JSON.stringify(data.relationships));
}
if (data.node_positions !== undefined) {
updateFields.push(`node_positions = $${paramIndex++}`);
values.push(
data.node_positions ? JSON.stringify(data.node_positions) : null
);
}
if (data.category !== undefined) {
updateFields.push(`category = $${paramIndex++}`);
values.push(data.category ? JSON.stringify(data.category) : null);
}
if (data.control !== undefined) {
updateFields.push(`control = $${paramIndex++}`);
values.push(data.control ? JSON.stringify(data.control) : null);
}
if (data.plan !== undefined) {
updateFields.push(`plan = $${paramIndex++}`);
values.push(data.plan ? JSON.stringify(data.plan) : null);
}
const updatedDiagram = await queryOne<any>(
`UPDATE dataflow_diagrams
SET ${updateFields.join(", ")}
WHERE diagram_id = $${paramIndex}
RETURNING *`,
[...values, diagramId]
);
// 업데이트 실행
const updatedDiagram = await prisma.dataflow_diagrams.update({
where: {
diagram_id: diagramId,
},
data: {
...(data.diagram_name && { diagram_name: data.diagram_name }),
...(data.relationships && {
relationships: data.relationships as Prisma.InputJsonValue,
}),
...(data.node_positions !== undefined && {
node_positions: data.node_positions
? (data.node_positions as Prisma.InputJsonValue)
: Prisma.JsonNull,
}),
...(data.category !== undefined && {
category: data.category
? (data.category as Prisma.InputJsonValue)
: undefined,
}),
...(data.control !== undefined && {
control: data.control as Prisma.InputJsonValue | undefined,
}),
...(data.plan !== undefined && {
plan: data.plan as Prisma.InputJsonValue | undefined,
}),
updated_by: data.updated_by,
updated_at: new Date(),
},
});
return updatedDiagram;
} catch (error) {
@ -254,27 +248,32 @@ export const deleteDataflowDiagram = async (
) => {
try {
// 먼저 해당 관계도가 존재하는지 확인
const whereConditions: string[] = ["diagram_id = $1"];
const values: any[] = [diagramId];
const whereClause: {
diagram_id: number;
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
whereConditions.push("company_code = $2");
values.push(companyCode);
whereClause.company_code = companyCode;
}
const existingDiagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
values
);
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
where: whereClause,
});
if (!existingDiagram) {
return false;
}
// 삭제 실행
await query(`DELETE FROM dataflow_diagrams WHERE diagram_id = $1`, [
diagramId,
]);
await prisma.dataflow_diagrams.delete({
where: {
diagram_id: diagramId,
},
});
return true;
} catch (error) {
@ -294,18 +293,21 @@ export const copyDataflowDiagram = async (
) => {
try {
// 원본 관계도 조회
const whereConditions: string[] = ["diagram_id = $1"];
const values: any[] = [diagramId];
const whereClause: {
diagram_id: number;
company_code?: string;
} = {
diagram_id: diagramId,
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
whereConditions.push("company_code = $2");
values.push(companyCode);
whereClause.company_code = companyCode;
}
const originalDiagram = await queryOne<any>(
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
values
);
const originalDiagram = await prisma.dataflow_diagrams.findFirst({
where: whereClause,
});
if (!originalDiagram) {
return null;
@ -323,19 +325,28 @@ export const copyDataflowDiagram = async (
: originalDiagram.diagram_name;
// 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기
const copyWhereConditions: string[] = ["diagram_name LIKE $1"];
const copyValues: any[] = [`${baseName}%`];
const copyWhereClause: {
diagram_name: {
startsWith: string;
};
company_code?: string;
} = {
diagram_name: {
startsWith: baseName,
},
};
// company_code가 '*'가 아닌 경우에만 필터링
if (companyCode !== "*") {
copyWhereConditions.push("company_code = $2");
copyValues.push(companyCode);
copyWhereClause.company_code = companyCode;
}
const existingCopies = await query<{ diagram_name: string }>(
`SELECT diagram_name FROM dataflow_diagrams
WHERE ${copyWhereConditions.join(" AND ")}`,
copyValues
);
const existingCopies = await prisma.dataflow_diagrams.findMany({
where: copyWhereClause,
select: {
diagram_name: true,
},
});
let maxNumber = 0;
existingCopies.forEach((copy) => {
@ -352,24 +363,19 @@ export const copyDataflowDiagram = async (
}
// 새로운 관계도 생성
const copiedDiagram = await queryOne<any>(
`INSERT INTO dataflow_diagrams
(diagram_name, relationships, node_positions, category, company_code,
created_by, updated_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
RETURNING *`,
[
copyName,
JSON.stringify(originalDiagram.relationships),
originalDiagram.node_positions
? JSON.stringify(originalDiagram.node_positions)
: null,
originalDiagram.category || null,
companyCode,
userId,
userId,
]
);
const copiedDiagram = await prisma.dataflow_diagrams.create({
data: {
diagram_name: copyName,
relationships: originalDiagram.relationships as Prisma.InputJsonValue,
node_positions: originalDiagram.node_positions
? (originalDiagram.node_positions as Prisma.InputJsonValue)
: Prisma.JsonNull,
category: originalDiagram.category || undefined,
company_code: companyCode,
created_by: userId,
updated_by: userId,
},
});
return copiedDiagram;
} catch (error) {
@ -384,39 +390,39 @@ export const copyDataflowDiagram = async (
*/
export const getAllRelationshipsForButtonControl = async (
companyCode: string
): Promise<
Array<{
id: string;
name: string;
sourceTable: string;
targetTable: string;
category: string;
}>
> => {
): Promise<Array<{
id: string;
name: string;
sourceTable: string;
targetTable: string;
category: string;
}>> => {
try {
logger.info(`전체 관계 목록 조회 시작 - companyCode: ${companyCode}`);
// dataflow_diagrams 테이블에서 관계도들을 조회
const diagrams = await query<{
diagram_id: number;
diagram_name: string;
relationships: any;
}>(
`SELECT diagram_id, diagram_name, relationships
FROM dataflow_diagrams
WHERE company_code = $1
ORDER BY updated_at DESC`,
[companyCode]
);
const diagrams = await prisma.dataflow_diagrams.findMany({
where: {
company_code: companyCode,
},
select: {
diagram_id: true,
diagram_name: true,
relationships: true,
},
orderBy: {
updated_at: "desc",
},
});
const allRelationships = diagrams.map((diagram) => {
// relationships 구조에서 테이블 정보 추출
const relationships = (diagram.relationships as any) || {};
const relationships = diagram.relationships as any || {};
// 테이블 정보 추출
let sourceTable = "";
let targetTable = "";
if (relationships.fromTable?.tableName) {
sourceTable = relationships.fromTable.tableName;
}

View File

@ -1,7 +1,7 @@
// 외부 DB 연결 서비스
// 작성일: 2024-12-17
import { query, queryOne } from "../database/db";
import prisma from "../config/database";
import {
ExternalDbConnection,
ExternalDbConnectionFilter,
@ -20,47 +20,43 @@ export class ExternalDbConnectionService {
filter: ExternalDbConnectionFilter
): Promise<ApiResponse<ExternalDbConnection[]>> {
try {
// WHERE 조건 동적 생성
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
const where: any = {};
// 필터 조건 적용
if (filter.db_type) {
whereConditions.push(`db_type = $${paramIndex++}`);
params.push(filter.db_type);
where.db_type = filter.db_type;
}
if (filter.is_active) {
whereConditions.push(`is_active = $${paramIndex++}`);
params.push(filter.is_active);
where.is_active = filter.is_active;
}
if (filter.company_code) {
whereConditions.push(`company_code = $${paramIndex++}`);
params.push(filter.company_code);
where.company_code = filter.company_code;
}
// 검색 조건 적용 (연결명 또는 설명에서 검색)
if (filter.search && filter.search.trim()) {
whereConditions.push(
`(connection_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
params.push(`%${filter.search.trim()}%`);
paramIndex++;
where.OR = [
{
connection_name: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
{
description: {
contains: filter.search.trim(),
mode: "insensitive",
},
},
];
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const connections = await query<any>(
`SELECT * FROM external_db_connections
${whereClause}
ORDER BY is_active DESC, connection_name ASC`,
params
);
const connections = await prisma.external_db_connections.findMany({
where,
orderBy: [{ is_active: "desc" }, { connection_name: "asc" }],
});
// 비밀번호는 반환하지 않음 (보안)
const safeConnections = connections.map((conn) => ({
@ -93,25 +89,26 @@ export class ExternalDbConnectionService {
try {
// 기본 연결 목록 조회
const connectionsResult = await this.getConnections(filter);
if (!connectionsResult.success || !connectionsResult.data) {
return {
success: false,
message: "연결 목록 조회에 실패했습니다.",
message: "연결 목록 조회에 실패했습니다."
};
}
// DB 타입 카테고리 정보 조회
const categories = await query<any>(
`SELECT * FROM db_type_categories
WHERE is_active = true
ORDER BY sort_order ASC, display_name ASC`,
[]
);
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
// DB 타입별로 그룹화
const groupedConnections: Record<string, any> = {};
// 카테고리 정보를 포함한 그룹 초기화
categories.forEach((category: any) => {
groupedConnections[category.type_code] = {
@ -120,36 +117,36 @@ export class ExternalDbConnectionService {
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order,
sort_order: category.sort_order
},
connections: [],
connections: []
};
});
// 연결을 해당 타입 그룹에 배치
connectionsResult.data.forEach((connection) => {
connectionsResult.data.forEach(connection => {
if (groupedConnections[connection.db_type]) {
groupedConnections[connection.db_type].connections.push(connection);
} else {
// 카테고리에 없는 DB 타입인 경우 기타 그룹에 추가
if (!groupedConnections["other"]) {
groupedConnections["other"] = {
if (!groupedConnections['other']) {
groupedConnections['other'] = {
category: {
type_code: "other",
display_name: "기타",
icon: "database",
color: "#6B7280",
sort_order: 999,
type_code: 'other',
display_name: '기타',
icon: 'database',
color: '#6B7280',
sort_order: 999
},
connections: [],
connections: []
};
}
groupedConnections["other"].connections.push(connection);
groupedConnections['other'].connections.push(connection);
}
});
// 연결이 없는 빈 그룹 제거
Object.keys(groupedConnections).forEach((key) => {
Object.keys(groupedConnections).forEach(key => {
if (groupedConnections[key].connections.length === 0) {
delete groupedConnections[key];
}
@ -158,14 +155,14 @@ export class ExternalDbConnectionService {
return {
success: true,
data: groupedConnections,
message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`,
message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`
};
} catch (error) {
console.error("그룹화된 연결 목록 조회 실패:", error);
return {
success: false,
message: "그룹화된 연결 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
@ -177,10 +174,9 @@ export class ExternalDbConnectionService {
id: number
): Promise<ApiResponse<ExternalDbConnection>> {
try {
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
if (!connection) {
return {
@ -218,10 +214,9 @@ export class ExternalDbConnectionService {
id: number
): Promise<ApiResponse<ExternalDbConnection>> {
try {
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
if (!connection) {
return {
@ -262,11 +257,13 @@ export class ExternalDbConnectionService {
this.validateConnectionData(data);
// 연결명 중복 확인
const existingConnection = await queryOne(
`SELECT id FROM external_db_connections
WHERE connection_name = $1 AND company_code = $2
LIMIT 1`,
[data.connection_name, data.company_code]
const existingConnection = await prisma.external_db_connections.findFirst(
{
where: {
connection_name: data.connection_name,
company_code: data.company_code,
},
}
);
if (existingConnection) {
@ -279,35 +276,30 @@ export class ExternalDbConnectionService {
// 비밀번호 암호화
const encryptedPassword = PasswordEncryption.encrypt(data.password);
const newConnection = await queryOne<any>(
`INSERT INTO external_db_connections (
connection_name, description, db_type, host, port, database_name,
username, password, connection_timeout, query_timeout, max_connections,
ssl_enabled, ssl_cert_path, connection_options, company_code, is_active,
created_by, updated_by, created_date, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW(), NOW())
RETURNING *`,
[
data.connection_name,
data.description,
data.db_type,
data.host,
data.port,
data.database_name,
data.username,
encryptedPassword,
data.connection_timeout,
data.query_timeout,
data.max_connections,
data.ssl_enabled,
data.ssl_cert_path,
JSON.stringify(data.connection_options),
data.company_code,
data.is_active,
data.created_by,
data.updated_by,
]
);
const newConnection = await prisma.external_db_connections.create({
data: {
connection_name: data.connection_name,
description: data.description,
db_type: data.db_type,
host: data.host,
port: data.port,
database_name: data.database_name,
username: data.username,
password: encryptedPassword,
connection_timeout: data.connection_timeout,
query_timeout: data.query_timeout,
max_connections: data.max_connections,
ssl_enabled: data.ssl_enabled,
ssl_cert_path: data.ssl_cert_path,
connection_options: data.connection_options as any,
company_code: data.company_code,
is_active: data.is_active,
created_by: data.created_by,
updated_by: data.updated_by,
created_date: new Date(),
updated_date: new Date(),
},
});
// 비밀번호는 반환하지 않음
const safeConnection = {
@ -340,10 +332,10 @@ export class ExternalDbConnectionService {
): Promise<ApiResponse<ExternalDbConnection>> {
try {
// 기존 연결 확인
const existingConnection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const existingConnection =
await prisma.external_db_connections.findUnique({
where: { id },
});
if (!existingConnection) {
return {
@ -354,18 +346,15 @@ export class ExternalDbConnectionService {
// 연결명 중복 확인 (자신 제외)
if (data.connection_name) {
const duplicateConnection = await queryOne(
`SELECT id FROM external_db_connections
WHERE connection_name = $1
AND company_code = $2
AND id != $3
LIMIT 1`,
[
data.connection_name,
data.company_code || existingConnection.company_code,
id,
]
);
const duplicateConnection =
await prisma.external_db_connections.findFirst({
where: {
connection_name: data.connection_name,
company_code:
data.company_code || existingConnection.company_code,
id: { not: id },
},
});
if (duplicateConnection) {
return {
@ -417,59 +406,23 @@ export class ExternalDbConnectionService {
}
// 업데이트 데이터 준비
const updates: string[] = [];
const updateParams: any[] = [];
let paramIndex = 1;
// 각 필드를 동적으로 추가
const fields = [
"connection_name",
"description",
"db_type",
"host",
"port",
"database_name",
"username",
"connection_timeout",
"query_timeout",
"max_connections",
"ssl_enabled",
"ssl_cert_path",
"connection_options",
"company_code",
"is_active",
"updated_by",
];
for (const field of fields) {
if (data[field as keyof ExternalDbConnection] !== undefined) {
updates.push(`${field} = $${paramIndex++}`);
const value = data[field as keyof ExternalDbConnection];
updateParams.push(
field === "connection_options" ? JSON.stringify(value) : value
);
}
}
const updateData: any = {
...data,
updated_date: new Date(),
};
// 비밀번호가 변경된 경우 암호화 (연결 테스트 통과 후)
if (data.password && data.password !== "***ENCRYPTED***") {
updates.push(`password = $${paramIndex++}`);
updateParams.push(PasswordEncryption.encrypt(data.password));
updateData.password = PasswordEncryption.encrypt(data.password);
} else {
// 비밀번호 필드 제거 (변경하지 않음)
delete updateData.password;
}
// updated_date는 항상 업데이트
updates.push(`updated_date = NOW()`);
// id 파라미터 추가
updateParams.push(id);
const updatedConnection = await queryOne<any>(
`UPDATE external_db_connections
SET ${updates.join(", ")}
WHERE id = $${paramIndex}
RETURNING *`,
updateParams
);
const updatedConnection = await prisma.external_db_connections.update({
where: { id },
data: updateData,
});
// 비밀번호는 반환하지 않음
const safeConnection = {
@ -498,10 +451,10 @@ export class ExternalDbConnectionService {
*/
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
try {
const existingConnection = await queryOne(
`SELECT id FROM external_db_connections WHERE id = $1`,
[id]
);
const existingConnection =
await prisma.external_db_connections.findUnique({
where: { id },
});
if (!existingConnection) {
return {
@ -511,7 +464,9 @@ export class ExternalDbConnectionService {
}
// 물리 삭제 (실제 데이터 삭제)
await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]);
await prisma.external_db_connections.delete({
where: { id },
});
return {
success: true,
@ -536,10 +491,9 @@ export class ExternalDbConnectionService {
): Promise<import("../types/externalDbTypes").ConnectionTestResult> {
try {
// 저장된 연결 정보 조회
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
if (!connection) {
return {
@ -720,10 +674,10 @@ export class ExternalDbConnectionService {
*/
static async getDecryptedPassword(id: number): Promise<string | null> {
try {
const connection = await queryOne<{ password: string }>(
`SELECT password FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
select: { password: true },
});
if (!connection) {
return null;
@ -747,10 +701,9 @@ export class ExternalDbConnectionService {
try {
// 연결 정보 조회
console.log("연결 정보 조회 시작:", { id });
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
console.log("조회된 연결 정보:", connection);
if (!connection) {
@ -800,25 +753,14 @@ export class ExternalDbConnectionService {
let result;
try {
const dbType = connection.db_type?.toLowerCase() || "postgresql";
const dbType = connection.db_type?.toLowerCase() || 'postgresql';
// 파라미터 바인딩을 지원하는 DB 타입들
const supportedDbTypes = [
"oracle",
"mysql",
"mariadb",
"postgresql",
"sqlite",
"sqlserver",
"mssql",
];
const supportedDbTypes = ['oracle', 'mysql', 'mariadb', 'postgresql', 'sqlite', 'sqlserver', 'mssql'];
if (supportedDbTypes.includes(dbType) && params.length > 0) {
// 파라미터 바인딩 지원 DB: 안전한 파라미터 바인딩 사용
logger.info(`${dbType.toUpperCase()} 파라미터 바인딩 실행:`, {
query,
params,
});
logger.info(`${dbType.toUpperCase()} 파라미터 바인딩 실행:`, { query, params });
result = await (connector as any).executeQuery(query, params);
} else {
// 파라미터가 없거나 지원하지 않는 DB: 기본 방식 사용
@ -904,10 +846,9 @@ export class ExternalDbConnectionService {
static async getTables(id: number): Promise<ApiResponse<TableInfo[]>> {
try {
// 연결 정보 조회
const connection = await queryOne<any>(
`SELECT * FROM external_db_connections WHERE id = $1`,
[id]
);
const connection = await prisma.external_db_connections.findUnique({
where: { id },
});
if (!connection) {
return {

View File

@ -1,4 +1,5 @@
import { query, queryOne } from "../database/db";
import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import {
CreateLayoutRequest,
UpdateLayoutRequest,
@ -76,59 +77,42 @@ export class LayoutService {
const skip = (page - 1) * size;
// 동적 WHERE 조건 구성
const whereConditions: string[] = ["is_active = $1"];
const values: any[] = ["Y"];
let paramIndex = 2;
// company_code OR is_public 조건
if (includePublic) {
whereConditions.push(
`(company_code = $${paramIndex} OR is_public = $${paramIndex + 1})`
);
values.push(companyCode, "Y");
paramIndex += 2;
} else {
whereConditions.push(`company_code = $${paramIndex++}`);
values.push(companyCode);
}
// 검색 조건 구성
const where: any = {
is_active: "Y",
OR: [
{ company_code: companyCode },
...(includePublic ? [{ is_public: "Y" }] : []),
],
};
if (category) {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
where.category = category;
}
if (layoutType) {
whereConditions.push(`layout_type = $${paramIndex++}`);
values.push(layoutType);
where.layout_type = layoutType;
}
if (searchTerm) {
whereConditions.push(
`(layout_name ILIKE $${paramIndex} OR layout_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
values.push(`%${searchTerm}%`);
paramIndex++;
where.OR = [
...where.OR,
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
{ layout_name_eng: { contains: searchTerm, mode: "insensitive" } },
{ description: { contains: searchTerm, mode: "insensitive" } },
];
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const [data, countResult] = await Promise.all([
query<any>(
`SELECT * FROM layout_standards
${whereClause}
ORDER BY sort_order ASC, created_date DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, skip]
),
queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM layout_standards ${whereClause}`,
values
),
const [data, total] = await Promise.all([
prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: [{ sort_order: "asc" }, { created_date: "desc" }],
}),
prisma.layout_standards.count({ where }),
]);
const total = parseInt(countResult?.count || "0");
return {
data: data.map(
(layout) =>
@ -165,13 +149,13 @@ export class LayoutService {
layoutCode: string,
companyCode: string
): Promise<LayoutStandard | null> {
const layout = await queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND is_active = $2
AND (company_code = $3 OR is_public = $4)
LIMIT 1`,
[layoutCode, "Y", companyCode, "Y"]
);
const layout = await prisma.layout_standards.findFirst({
where: {
layout_code: layoutCode,
is_active: "Y",
OR: [{ company_code: companyCode }, { is_public: "Y" }],
},
});
if (!layout) return null;
@ -212,31 +196,24 @@ export class LayoutService {
companyCode
);
const layout = await queryOne<any>(
`INSERT INTO layout_standards
(layout_code, layout_name, layout_name_eng, description, layout_type, category,
icon_name, default_size, layout_config, zones_config, is_public, is_active,
company_code, created_by, updated_by, created_date, updated_date, sort_order)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW(), 0)
RETURNING *`,
[
layoutCode,
request.layoutName,
request.layoutNameEng,
request.description,
request.layoutType,
request.category,
request.iconName,
safeJSONStringify(request.defaultSize),
safeJSONStringify(request.layoutConfig),
safeJSONStringify(request.zonesConfig),
request.isPublic ? "Y" : "N",
"Y",
companyCode,
userId,
userId,
]
);
const layout = await prisma.layout_standards.create({
data: {
layout_code: layoutCode,
layout_name: request.layoutName,
layout_name_eng: request.layoutNameEng,
description: request.description,
layout_type: request.layoutType,
category: request.category,
icon_name: request.iconName,
default_size: safeJSONStringify(request.defaultSize) as any,
layout_config: safeJSONStringify(request.layoutConfig) as any,
zones_config: safeJSONStringify(request.zonesConfig) as any,
is_public: request.isPublic ? "Y" : "N",
company_code: companyCode,
created_by: userId,
updated_by: userId,
},
});
return this.mapToLayoutStandard(layout);
}
@ -250,69 +227,47 @@ export class LayoutService {
userId: string
): Promise<LayoutStandard | null> {
// 수정 권한 확인
const existing = await queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
[request.layoutCode, companyCode, "Y"]
);
const existing = await prisma.layout_standards.findFirst({
where: {
layout_code: request.layoutCode,
company_code: companyCode,
is_active: "Y",
},
});
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
}
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_by = $1", "updated_date = NOW()"];
const values: any[] = [userId];
let paramIndex = 2;
const updateData: any = {
updated_by: userId,
updated_date: new Date(),
};
if (request.layoutName !== undefined) {
updateFields.push(`layout_name = $${paramIndex++}`);
values.push(request.layoutName);
}
if (request.layoutNameEng !== undefined) {
updateFields.push(`layout_name_eng = $${paramIndex++}`);
values.push(request.layoutNameEng);
}
if (request.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(request.description);
}
if (request.layoutType !== undefined) {
updateFields.push(`layout_type = $${paramIndex++}`);
values.push(request.layoutType);
}
if (request.category !== undefined) {
updateFields.push(`category = $${paramIndex++}`);
values.push(request.category);
}
if (request.iconName !== undefined) {
updateFields.push(`icon_name = $${paramIndex++}`);
values.push(request.iconName);
}
if (request.defaultSize !== undefined) {
updateFields.push(`default_size = $${paramIndex++}`);
values.push(safeJSONStringify(request.defaultSize));
}
if (request.layoutConfig !== undefined) {
updateFields.push(`layout_config = $${paramIndex++}`);
values.push(safeJSONStringify(request.layoutConfig));
}
if (request.zonesConfig !== undefined) {
updateFields.push(`zones_config = $${paramIndex++}`);
values.push(safeJSONStringify(request.zonesConfig));
}
if (request.isPublic !== undefined) {
updateFields.push(`is_public = $${paramIndex++}`);
values.push(request.isPublic ? "Y" : "N");
}
// 수정할 필드만 업데이트
if (request.layoutName !== undefined)
updateData.layout_name = request.layoutName;
if (request.layoutNameEng !== undefined)
updateData.layout_name_eng = request.layoutNameEng;
if (request.description !== undefined)
updateData.description = request.description;
if (request.layoutType !== undefined)
updateData.layout_type = request.layoutType;
if (request.category !== undefined) updateData.category = request.category;
if (request.iconName !== undefined) updateData.icon_name = request.iconName;
if (request.defaultSize !== undefined)
updateData.default_size = safeJSONStringify(request.defaultSize) as any;
if (request.layoutConfig !== undefined)
updateData.layout_config = safeJSONStringify(request.layoutConfig) as any;
if (request.zonesConfig !== undefined)
updateData.zones_config = safeJSONStringify(request.zonesConfig) as any;
if (request.isPublic !== undefined)
updateData.is_public = request.isPublic ? "Y" : "N";
const updated = await queryOne<any>(
`UPDATE layout_standards
SET ${updateFields.join(", ")}
WHERE layout_code = $${paramIndex}
RETURNING *`,
[...values, request.layoutCode]
);
const updated = await prisma.layout_standards.update({
where: { layout_code: request.layoutCode },
data: updateData,
});
return this.mapToLayoutStandard(updated);
}
@ -325,22 +280,26 @@ export class LayoutService {
companyCode: string,
userId: string
): Promise<boolean> {
const existing = await queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
[layoutCode, companyCode, "Y"]
);
const existing = await prisma.layout_standards.findFirst({
where: {
layout_code: layoutCode,
company_code: companyCode,
is_active: "Y",
},
});
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
}
await query(
`UPDATE layout_standards
SET is_active = $1, updated_by = $2, updated_date = NOW()
WHERE layout_code = $3`,
["N", userId, layoutCode]
);
await prisma.layout_standards.update({
where: { layout_code: layoutCode },
data: {
is_active: "N",
updated_by: userId,
updated_date: new Date(),
},
});
return true;
}
@ -383,17 +342,20 @@ export class LayoutService {
async getLayoutCountsByCategory(
companyCode: string
): Promise<Record<string, number>> {
const counts = await query<{ category: string; count: string }>(
`SELECT category, COUNT(*) as count
FROM layout_standards
WHERE is_active = $1 AND (company_code = $2 OR is_public = $3)
GROUP BY category`,
["Y", companyCode, "Y"]
);
const counts = await prisma.layout_standards.groupBy({
by: ["category"],
_count: {
layout_code: true,
},
where: {
is_active: "Y",
OR: [{ company_code: companyCode }, { is_public: "Y" }],
},
});
return counts.reduce(
(acc: Record<string, number>, item: any) => {
acc[item.category] = parseInt(item.count);
acc[item.category] = item._count.layout_code;
return acc;
},
{} as Record<string, number>
@ -408,11 +370,16 @@ export class LayoutService {
companyCode: string
): Promise<string> {
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
const existingCodes = await query<{ layout_code: string }>(
`SELECT layout_code FROM layout_standards
WHERE layout_code LIKE $1`,
[`${prefix}%`]
);
const existingCodes = await prisma.layout_standards.findMany({
where: {
layout_code: {
startsWith: prefix,
},
},
select: {
layout_code: true,
},
});
const maxNumber = existingCodes.reduce((max: number, item: any) => {
const match = item.layout_code.match(/_(\d+)$/);

View File

@ -1,4 +1,4 @@
import { query, queryOne, transaction } from "../database/db";
import { PrismaClient } from "@prisma/client";
import { logger } from "../utils/logger";
import {
Language,
@ -15,6 +15,8 @@ import {
ApiResponse,
} from "../types/multilang";
const prisma = new PrismaClient();
export class MultiLangService {
constructor() {}
@ -25,27 +27,25 @@ export class MultiLangService {
try {
logger.info("언어 목록 조회 시작");
const languages = await query<{
lang_code: string;
lang_name: string;
lang_native: string | null;
is_active: string | null;
sort_order: number | null;
created_date: Date | null;
created_by: string | null;
updated_date: Date | null;
updated_by: string | null;
}>(
`SELECT lang_code, lang_name, lang_native, is_active, sort_order,
created_date, created_by, updated_date, updated_by
FROM language_master
ORDER BY sort_order ASC, lang_code ASC`
);
const languages = await prisma.language_master.findMany({
orderBy: [{ sort_order: "asc" }, { lang_code: "asc" }],
select: {
lang_code: true,
lang_name: true,
lang_native: true,
is_active: true,
sort_order: true,
created_date: true,
created_by: true,
updated_date: true,
updated_by: true,
},
});
const mappedLanguages: Language[] = languages.map((lang) => ({
langCode: lang.lang_code,
langName: lang.lang_name,
langNative: lang.lang_native || "",
langNative: lang.lang_native,
isActive: lang.is_active || "N",
sortOrder: lang.sort_order ?? undefined,
createdDate: lang.created_date || undefined,
@ -72,10 +72,9 @@ export class MultiLangService {
logger.info("언어 생성 시작", { languageData });
// 중복 체크
const existingLanguage = await queryOne<{ lang_code: string }>(
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
[languageData.langCode]
);
const existingLanguage = await prisma.language_master.findUnique({
where: { lang_code: languageData.langCode },
});
if (existingLanguage) {
throw new Error(
@ -84,44 +83,30 @@ export class MultiLangService {
}
// 언어 생성
const createdLanguage = await queryOne<{
lang_code: string;
lang_name: string;
lang_native: string | null;
is_active: string | null;
sort_order: number | null;
created_date: Date | null;
created_by: string | null;
updated_date: Date | null;
updated_by: string | null;
}>(
`INSERT INTO language_master
(lang_code, lang_name, lang_native, is_active, sort_order, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[
languageData.langCode,
languageData.langName,
languageData.langNative,
languageData.isActive || "Y",
languageData.sortOrder || 0,
languageData.createdBy || "system",
languageData.updatedBy || "system",
]
);
const createdLanguage = await prisma.language_master.create({
data: {
lang_code: languageData.langCode,
lang_name: languageData.langName,
lang_native: languageData.langNative,
is_active: languageData.isActive || "Y",
sort_order: languageData.sortOrder || 0,
created_by: languageData.createdBy || "system",
updated_by: languageData.updatedBy || "system",
},
});
logger.info("언어 생성 완료", { langCode: createdLanguage!.lang_code });
logger.info("언어 생성 완료", { langCode: createdLanguage.lang_code });
return {
langCode: createdLanguage!.lang_code,
langName: createdLanguage!.lang_name,
langNative: createdLanguage!.lang_native || "",
isActive: createdLanguage!.is_active || "N",
sortOrder: createdLanguage!.sort_order ?? undefined,
createdDate: createdLanguage!.created_date || undefined,
createdBy: createdLanguage!.created_by || undefined,
updatedDate: createdLanguage!.updated_date || undefined,
updatedBy: createdLanguage!.updated_by || undefined,
langCode: createdLanguage.lang_code,
langName: createdLanguage.lang_name,
langNative: createdLanguage.lang_native,
isActive: createdLanguage.is_active || "N",
sortOrder: createdLanguage.sort_order ?? undefined,
createdDate: createdLanguage.created_date || undefined,
createdBy: createdLanguage.created_by || undefined,
updatedDate: createdLanguage.updated_date || undefined,
updatedBy: createdLanguage.updated_by || undefined,
};
} catch (error) {
logger.error("언어 생성 중 오류 발생:", error);
@ -142,72 +127,42 @@ export class MultiLangService {
logger.info("언어 수정 시작", { langCode, languageData });
// 기존 언어 확인
const existingLanguage = await queryOne<{ lang_code: string }>(
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
[langCode]
);
const existingLanguage = await prisma.language_master.findUnique({
where: { lang_code: langCode },
});
if (!existingLanguage) {
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
}
// 동적 UPDATE 쿼리 생성
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (languageData.langName) {
updates.push(`lang_name = $${paramIndex++}`);
values.push(languageData.langName);
}
if (languageData.langNative) {
updates.push(`lang_native = $${paramIndex++}`);
values.push(languageData.langNative);
}
if (languageData.isActive) {
updates.push(`is_active = $${paramIndex++}`);
values.push(languageData.isActive);
}
if (languageData.sortOrder !== undefined) {
updates.push(`sort_order = $${paramIndex++}`);
values.push(languageData.sortOrder);
}
updates.push(`updated_by = $${paramIndex++}`);
values.push(languageData.updatedBy || "system");
values.push(langCode); // WHERE 조건용
// 언어 수정
const updatedLanguage = await queryOne<{
lang_code: string;
lang_name: string;
lang_native: string | null;
is_active: string | null;
sort_order: number | null;
created_date: Date | null;
created_by: string | null;
updated_date: Date | null;
updated_by: string | null;
}>(
`UPDATE language_master SET ${updates.join(", ")}
WHERE lang_code = $${paramIndex}
RETURNING *`,
values
);
const updatedLanguage = await prisma.language_master.update({
where: { lang_code: langCode },
data: {
...(languageData.langName && { lang_name: languageData.langName }),
...(languageData.langNative && {
lang_native: languageData.langNative,
}),
...(languageData.isActive && { is_active: languageData.isActive }),
...(languageData.sortOrder !== undefined && {
sort_order: languageData.sortOrder,
}),
updated_by: languageData.updatedBy || "system",
},
});
logger.info("언어 수정 완료", { langCode });
return {
langCode: updatedLanguage!.lang_code,
langName: updatedLanguage!.lang_name,
langNative: updatedLanguage!.lang_native || "",
isActive: updatedLanguage!.is_active || "N",
sortOrder: updatedLanguage!.sort_order ?? undefined,
createdDate: updatedLanguage!.created_date || undefined,
createdBy: updatedLanguage!.created_by || undefined,
updatedDate: updatedLanguage!.updated_date || undefined,
updatedBy: updatedLanguage!.updated_by || undefined,
langCode: updatedLanguage.lang_code,
langName: updatedLanguage.lang_name,
langNative: updatedLanguage.lang_native,
isActive: updatedLanguage.is_active || "N",
sortOrder: updatedLanguage.sort_order ?? undefined,
createdDate: updatedLanguage.created_date || undefined,
createdBy: updatedLanguage.created_by || undefined,
updatedDate: updatedLanguage.updated_date || undefined,
updatedBy: updatedLanguage.updated_by || undefined,
};
} catch (error) {
logger.error("언어 수정 중 오류 발생:", error);
@ -225,10 +180,10 @@ export class MultiLangService {
logger.info("언어 상태 토글 시작", { langCode });
// 현재 언어 조회
const currentLanguage = await queryOne<{ is_active: string | null }>(
`SELECT is_active FROM language_master WHERE lang_code = $1`,
[langCode]
);
const currentLanguage = await prisma.language_master.findUnique({
where: { lang_code: langCode },
select: { is_active: true },
});
if (!currentLanguage) {
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
@ -237,12 +192,13 @@ export class MultiLangService {
const newStatus = currentLanguage.is_active === "Y" ? "N" : "Y";
// 상태 업데이트
await query(
`UPDATE language_master
SET is_active = $1, updated_by = $2
WHERE lang_code = $3`,
[newStatus, "system", langCode]
);
await prisma.language_master.update({
where: { lang_code: langCode },
data: {
is_active: newStatus,
updated_by: "system",
},
});
const result = newStatus === "Y" ? "활성화" : "비활성화";
logger.info("언어 상태 토글 완료", { langCode, result });
@ -263,55 +219,47 @@ export class MultiLangService {
try {
logger.info("다국어 키 목록 조회 시작", { params });
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
const whereConditions: any = {};
// 회사 코드 필터
if (params.companyCode) {
whereConditions.push(`company_code = $${paramIndex++}`);
values.push(params.companyCode);
whereConditions.company_code = params.companyCode;
}
// 메뉴 코드 필터
if (params.menuCode) {
whereConditions.push(`menu_name = $${paramIndex++}`);
values.push(params.menuCode);
whereConditions.menu_name = params.menuCode;
}
// 검색 조건 (OR)
// 검색 조건
if (params.searchText) {
whereConditions.push(
`(lang_key ILIKE $${paramIndex} OR description ILIKE $${paramIndex} OR menu_name ILIKE $${paramIndex})`
);
values.push(`%${params.searchText}%`);
paramIndex++;
whereConditions.OR = [
{ lang_key: { contains: params.searchText, mode: "insensitive" } },
{ description: { contains: params.searchText, mode: "insensitive" } },
{ menu_name: { contains: params.searchText, mode: "insensitive" } },
];
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const langKeys = await query<{
key_id: number;
company_code: string;
menu_name: string | null;
lang_key: string;
description: string | null;
is_active: string | null;
created_date: Date | null;
created_by: string | null;
updated_date: Date | null;
updated_by: string | null;
}>(
`SELECT key_id, company_code, menu_name, lang_key, description, is_active,
created_date, created_by, updated_date, updated_by
FROM multi_lang_key_master
${whereClause}
ORDER BY company_code ASC, menu_name ASC, lang_key ASC`,
values
);
const langKeys = await prisma.multi_lang_key_master.findMany({
where: whereConditions,
orderBy: [
{ company_code: "asc" },
{ menu_name: "asc" },
{ lang_key: "asc" },
],
select: {
key_id: true,
company_code: true,
menu_name: true,
lang_key: true,
description: true,
is_active: true,
created_date: true,
created_by: true,
updated_date: true,
updated_by: true,
},
});
const mappedKeys: LangKey[] = langKeys.map((key) => ({
keyId: key.key_id,
@ -343,24 +291,24 @@ export class MultiLangService {
try {
logger.info("다국어 텍스트 조회 시작", { keyId });
const langTexts = await query<{
text_id: number;
key_id: number;
lang_code: string;
lang_text: string;
is_active: string | null;
created_date: Date | null;
created_by: string | null;
updated_date: Date | null;
updated_by: string | null;
}>(
`SELECT text_id, key_id, lang_code, lang_text, is_active,
created_date, created_by, updated_date, updated_by
FROM multi_lang_text
WHERE key_id = $1 AND is_active = $2
ORDER BY lang_code ASC`,
[keyId, "Y"]
);
const langTexts = await prisma.multi_lang_text.findMany({
where: {
key_id: keyId,
is_active: "Y",
},
orderBy: { lang_code: "asc" },
select: {
text_id: true,
key_id: true,
lang_code: true,
lang_text: true,
is_active: true,
created_date: true,
created_by: true,
updated_date: true,
updated_by: true,
},
});
const mappedTexts: LangText[] = langTexts.map((text) => ({
textId: text.text_id,
@ -392,11 +340,12 @@ export class MultiLangService {
logger.info("다국어 키 생성 시작", { keyData });
// 중복 체크
const existingKey = await queryOne<{ key_id: number }>(
`SELECT key_id FROM multi_lang_key_master
WHERE company_code = $1 AND lang_key = $2`,
[keyData.companyCode, keyData.langKey]
);
const existingKey = await prisma.multi_lang_key_master.findFirst({
where: {
company_code: keyData.companyCode,
lang_key: keyData.langKey,
},
});
if (existingKey) {
throw new Error(
@ -405,28 +354,24 @@ export class MultiLangService {
}
// 다국어 키 생성
const createdKey = await queryOne<{ key_id: number }>(
`INSERT INTO multi_lang_key_master
(company_code, menu_name, lang_key, description, is_active, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING key_id`,
[
keyData.companyCode,
keyData.menuName || null,
keyData.langKey,
keyData.description || null,
keyData.isActive || "Y",
keyData.createdBy || "system",
keyData.updatedBy || "system",
]
);
const createdKey = await prisma.multi_lang_key_master.create({
data: {
company_code: keyData.companyCode,
menu_name: keyData.menuName || null,
lang_key: keyData.langKey,
description: keyData.description || null,
is_active: keyData.isActive || "Y",
created_by: keyData.createdBy || "system",
updated_by: keyData.updatedBy || "system",
},
});
logger.info("다국어 키 생성 완료", {
keyId: createdKey!.key_id,
keyId: createdKey.key_id,
langKey: keyData.langKey,
});
return createdKey!.key_id;
return createdKey.key_id;
} catch (error) {
logger.error("다국어 키 생성 중 오류 발생:", error);
throw new Error(
@ -446,10 +391,9 @@ export class MultiLangService {
logger.info("다국어 키 수정 시작", { keyId, keyData });
// 기존 키 확인
const existingKey = await queryOne<{ key_id: number }>(
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
[keyId]
);
const existingKey = await prisma.multi_lang_key_master.findUnique({
where: { key_id: keyId },
});
if (!existingKey) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
@ -457,11 +401,13 @@ export class MultiLangService {
// 중복 체크 (자신을 제외하고)
if (keyData.companyCode && keyData.langKey) {
const duplicateKey = await queryOne<{ key_id: number }>(
`SELECT key_id FROM multi_lang_key_master
WHERE company_code = $1 AND lang_key = $2 AND key_id != $3`,
[keyData.companyCode, keyData.langKey, keyId]
);
const duplicateKey = await prisma.multi_lang_key_master.findFirst({
where: {
company_code: keyData.companyCode,
lang_key: keyData.langKey,
key_id: { not: keyId },
},
});
if (duplicateKey) {
throw new Error(
@ -470,39 +416,21 @@ export class MultiLangService {
}
}
// 동적 UPDATE 쿼리 생성
const updates: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (keyData.companyCode) {
updates.push(`company_code = $${paramIndex++}`);
values.push(keyData.companyCode);
}
if (keyData.menuName !== undefined) {
updates.push(`menu_name = $${paramIndex++}`);
values.push(keyData.menuName);
}
if (keyData.langKey) {
updates.push(`lang_key = $${paramIndex++}`);
values.push(keyData.langKey);
}
if (keyData.description !== undefined) {
updates.push(`description = $${paramIndex++}`);
values.push(keyData.description);
}
updates.push(`updated_by = $${paramIndex++}`);
values.push(keyData.updatedBy || "system");
values.push(keyId); // WHERE 조건용
// 다국어 키 수정
await query(
`UPDATE multi_lang_key_master SET ${updates.join(", ")}
WHERE key_id = $${paramIndex}`,
values
);
await prisma.multi_lang_key_master.update({
where: { key_id: keyId },
data: {
...(keyData.companyCode && { company_code: keyData.companyCode }),
...(keyData.menuName !== undefined && {
menu_name: keyData.menuName,
}),
...(keyData.langKey && { lang_key: keyData.langKey }),
...(keyData.description !== undefined && {
description: keyData.description,
}),
updated_by: keyData.updatedBy || "system",
},
});
logger.info("다국어 키 수정 완료", { keyId });
} catch (error) {
@ -521,27 +449,25 @@ export class MultiLangService {
logger.info("다국어 키 삭제 시작", { keyId });
// 기존 키 확인
const existingKey = await queryOne<{ key_id: number }>(
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
[keyId]
);
const existingKey = await prisma.multi_lang_key_master.findUnique({
where: { key_id: keyId },
});
if (!existingKey) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
// 트랜잭션으로 키와 연관된 텍스트 모두 삭제
await transaction(async (client) => {
await prisma.$transaction(async (tx) => {
// 관련된 다국어 텍스트 삭제
await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [
keyId,
]);
await tx.multi_lang_text.deleteMany({
where: { key_id: keyId },
});
// 다국어 키 삭제
await client.query(
`DELETE FROM multi_lang_key_master WHERE key_id = $1`,
[keyId]
);
await tx.multi_lang_key_master.delete({
where: { key_id: keyId },
});
});
logger.info("다국어 키 삭제 완료", { keyId });
@ -561,10 +487,10 @@ export class MultiLangService {
logger.info("다국어 키 상태 토글 시작", { keyId });
// 현재 키 조회
const currentKey = await queryOne<{ is_active: string | null }>(
`SELECT is_active FROM multi_lang_key_master WHERE key_id = $1`,
[keyId]
);
const currentKey = await prisma.multi_lang_key_master.findUnique({
where: { key_id: keyId },
select: { is_active: true },
});
if (!currentKey) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
@ -573,12 +499,13 @@ export class MultiLangService {
const newStatus = currentKey.is_active === "Y" ? "N" : "Y";
// 상태 업데이트
await query(
`UPDATE multi_lang_key_master
SET is_active = $1, updated_by = $2
WHERE key_id = $3`,
[newStatus, "system", keyId]
);
await prisma.multi_lang_key_master.update({
where: { key_id: keyId },
data: {
is_active: newStatus,
updated_by: "system",
},
});
const result = newStatus === "Y" ? "활성화" : "비활성화";
logger.info("다국어 키 상태 토글 완료", { keyId, result });
@ -606,39 +533,33 @@ export class MultiLangService {
});
// 기존 키 확인
const existingKey = await queryOne<{ key_id: number }>(
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
[keyId]
);
const existingKey = await prisma.multi_lang_key_master.findUnique({
where: { key_id: keyId },
});
if (!existingKey) {
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
}
// 트랜잭션으로 기존 텍스트 삭제 후 새로 생성
await transaction(async (client) => {
await prisma.$transaction(async (tx) => {
// 기존 텍스트 삭제
await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [
keyId,
]);
await tx.multi_lang_text.deleteMany({
where: { key_id: keyId },
});
// 새로운 텍스트 삽입
if (textData.texts.length > 0) {
for (const text of textData.texts) {
await client.query(
`INSERT INTO multi_lang_text
(key_id, lang_code, lang_text, is_active, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
keyId,
text.langCode,
text.langText,
text.isActive || "Y",
text.createdBy || "system",
text.updatedBy || "system",
]
);
}
await tx.multi_lang_text.createMany({
data: textData.texts.map((text) => ({
key_id: keyId,
lang_code: text.langCode,
lang_text: text.langText,
is_active: text.isActive || "Y",
created_by: text.createdBy || "system",
updated_by: text.updatedBy || "system",
})),
});
}
});
@ -661,25 +582,21 @@ export class MultiLangService {
try {
logger.info("사용자별 다국어 텍스트 조회 시작", { params });
const result = await queryOne<{ lang_text: string }>(
`SELECT mlt.lang_text
FROM multi_lang_text mlt
INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id
WHERE mlt.lang_code = $1
AND mlt.is_active = $2
AND mlkm.company_code = $3
AND mlkm.menu_name = $4
AND mlkm.lang_key = $5
AND mlkm.is_active = $6`,
[
params.userLang,
"Y",
params.companyCode,
params.menuCode,
params.langKey,
"Y",
]
);
const result = await prisma.multi_lang_text.findFirst({
where: {
lang_code: params.userLang,
is_active: "Y",
multi_lang_key_master: {
company_code: params.companyCode,
menu_name: params.menuCode,
lang_key: params.langKey,
is_active: "Y",
},
},
select: {
lang_text: true,
},
});
if (!result) {
logger.warn("사용자별 다국어 텍스트를 찾을 수 없음", { params });
@ -715,17 +632,20 @@ export class MultiLangService {
langCode,
});
const result = await queryOne<{ lang_text: string }>(
`SELECT mlt.lang_text
FROM multi_lang_text mlt
INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id
WHERE mlt.lang_code = $1
AND mlt.is_active = $2
AND mlkm.company_code = $3
AND mlkm.lang_key = $4
AND mlkm.is_active = $5`,
[langCode, "Y", companyCode, langKey, "Y"]
);
const result = await prisma.multi_lang_text.findFirst({
where: {
lang_code: langCode,
is_active: "Y",
multi_lang_key_master: {
company_code: companyCode,
lang_key: langKey,
is_active: "Y",
},
},
select: {
lang_text: true,
},
});
if (!result) {
logger.warn("특정 키의 다국어 텍스트를 찾을 수 없음", {
@ -771,26 +691,31 @@ export class MultiLangService {
}
// 모든 키에 대한 번역 조회
const placeholders = params.langKeys
.map((_, i) => `$${i + 4}`)
.join(", ");
const translations = await query<{
lang_text: string;
lang_key: string;
company_code: string;
}>(
`SELECT mlt.lang_text, mlkm.lang_key, mlkm.company_code
FROM multi_lang_text mlt
INNER JOIN multi_lang_key_master mlkm ON mlt.key_id = mlkm.key_id
WHERE mlt.lang_code = $1
AND mlt.is_active = $2
AND mlkm.lang_key IN (${placeholders})
AND mlkm.company_code IN ($3, '*')
AND mlkm.is_active = $2
ORDER BY mlkm.company_code ASC`,
[params.userLang, "Y", params.companyCode, ...params.langKeys]
);
const translations = await prisma.multi_lang_text.findMany({
where: {
lang_code: params.userLang,
is_active: "Y",
multi_lang_key_master: {
lang_key: { in: params.langKeys },
company_code: { in: [params.companyCode, "*"] },
is_active: "Y",
},
},
select: {
lang_text: true,
multi_lang_key_master: {
select: {
lang_key: true,
company_code: true,
},
},
},
orderBy: {
multi_lang_key_master: {
company_code: "asc", // 회사별 우선, '*' 는 기본값
},
},
});
const result: Record<string, string> = {};
@ -801,7 +726,7 @@ export class MultiLangService {
// 실제 번역으로 덮어쓰기 (회사별 우선)
translations.forEach((translation) => {
const langKey = translation.lang_key;
const langKey = translation.multi_lang_key_master.lang_key;
if (params.langKeys.includes(langKey)) {
result[langKey] = translation.lang_text;
}
@ -830,31 +755,29 @@ export class MultiLangService {
logger.info("언어 삭제 시작", { langCode });
// 기존 언어 확인
const existingLanguage = await queryOne<{ lang_code: string }>(
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
[langCode]
);
const existingLanguage = await prisma.language_master.findUnique({
where: { lang_code: langCode },
});
if (!existingLanguage) {
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
}
// 트랜잭션으로 언어와 관련 텍스트 삭제
await transaction(async (client) => {
await prisma.$transaction(async (tx) => {
// 해당 언어의 다국어 텍스트 삭제
const deleteResult = await client.query(
`DELETE FROM multi_lang_text WHERE lang_code = $1`,
[langCode]
);
const deleteResult = await tx.multi_lang_text.deleteMany({
where: { lang_code: langCode },
});
logger.info(`삭제된 다국어 텍스트 수: ${deleteResult.rowCount}`, {
logger.info(`삭제된 다국어 텍스트 수: ${deleteResult.count}`, {
langCode,
});
// 언어 마스터 삭제
await client.query(`DELETE FROM language_master WHERE lang_code = $1`, [
langCode,
]);
await tx.language_master.delete({
where: { lang_code: langCode },
});
});
logger.info("언어 삭제 완료", { langCode });