Compare commits
15 Commits
57f1d8274e
...
4c20d93c87
| Author | SHA1 | Date |
|---|---|---|
|
|
4c20d93c87 | |
|
|
45ec38790b | |
|
|
7fb2ce582c | |
|
|
34295d6afa | |
|
|
296340351f | |
|
|
a5653eee3e | |
|
|
2331e3fd20 | |
|
|
c37b74a8bb | |
|
|
37c4f6a450 | |
|
|
143f851190 | |
|
|
284c67193d | |
|
|
244c47db35 | |
|
|
399afc62d8 | |
|
|
e5180b7659 | |
|
|
5f3f869135 |
|
|
@ -6,15 +6,16 @@ DynamicFormService는 **13개의 Prisma 호출**이 있습니다. 대부분(약
|
|||
|
||||
### 📊 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| 파일 위치 | `backend-node/src/services/dynamicFormService.ts` |
|
||||
| 파일 크기 | 1,200+ 라인 |
|
||||
| Prisma 호출 | 13개 ($queryRaw: 11개, ORM: 2개) |
|
||||
| **현재 진행률** | **0/13 (0%)** ⏳ **전환 필요** |
|
||||
| **전환 필요** | **13개 모두 전환 필요** (SQL은 이미 작성되어 있음) |
|
||||
| 복잡도 | 낮음 (SQL 작성은 완료, `query()` 함수로 교체만 필요) |
|
||||
| 우선순위 | 🟢 낮음 (Phase 2.4) |
|
||||
| 항목 | 내용 |
|
||||
| --------------- | ------------------------------------------------- |
|
||||
| 파일 위치 | `backend-node/src/services/dynamicFormService.ts` |
|
||||
| 파일 크기 | 1,213 라인 |
|
||||
| Prisma 호출 | 0개 (전환 완료) |
|
||||
| **현재 진행률** | **13/13 (100%)** ✅ **완료** |
|
||||
| **전환 상태** | **Raw Query로 전환 완료** |
|
||||
| 복잡도 | 낮음 (SQL 작성 완료 → `query()` 함수로 교체 완료) |
|
||||
| 우선순위 | 🟢 낮음 (Phase 2.4) |
|
||||
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
|
||||
|
||||
### 🎯 전환 목표
|
||||
|
||||
|
|
@ -167,52 +168,63 @@ 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 호출 전환 (2개 함수) ⏳ **진행 예정**
|
||||
### 1단계: ORM 호출 전환 ✅ **완료**
|
||||
|
||||
- [ ] `getFormDataById()` - dynamic_form_data.findUnique
|
||||
- [ ] `getScreenLayoutsForControl()` - screen_layouts.findMany
|
||||
- [x] `getFormDataById()` - queryOne 전환
|
||||
- [x] `getScreenLayoutsForControl()` - query 전환
|
||||
- [x] 모든 Raw Query 함수 전환
|
||||
|
||||
### 2단계: 테스트 & 검증 ⏳ **진행 예정**
|
||||
|
||||
- [ ] 단위 테스트 작성 (5개)
|
||||
- [ ] 통합 테스트 작성 (3개 시나리오)
|
||||
- [ ] Prisma import 완전 제거 확인
|
||||
- [x] Prisma import 완전 제거 확인 ✅
|
||||
- [ ] 성능 테스트
|
||||
|
||||
---
|
||||
|
||||
## 🎯 완료 기준
|
||||
|
||||
- [ ] **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 쿼리)
|
||||
- [x] **13개 모든 Prisma 호출을 Raw Query로 전환 완료** ✅
|
||||
- [x] 11개 `$queryRaw` → `query()` 함수로 교체 ✅
|
||||
- [x] 2개 ORM 메서드 → `query()` 함수로 전환 ✅
|
||||
- [x] **모든 TypeScript 컴파일 오류 해결** ✅
|
||||
- [x] **`import prisma` 완전 제거** ✅
|
||||
- [ ] **모든 단위 테스트 통과 (5개)** ⏳
|
||||
- [ ] **모든 통합 테스트 작성 완료 (3개 시나리오)** ⏳
|
||||
- [ ] **성능 저하 없음** ⏳
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-09-30
|
||||
**예상 소요 시간**: 0.5일 (SQL은 85% 작성 완료, 함수 교체 작업 필요)
|
||||
**완료일**: 2025-10-01
|
||||
**소요 시간**: 완료됨 (이전에 전환)
|
||||
**담당자**: 백엔드 개발팀
|
||||
**우선순위**: 🟢 낮음 (Phase 2.4)
|
||||
**상태**: ⏳ **진행 예정**
|
||||
**특이사항**: SQL은 거의 작성되어 있어 `prisma.$queryRaw` → `query()` 단순 교체 작업이 주요 작업
|
||||
**상태**: ✅ **전환 완료** (테스트 필요)
|
||||
**특이사항**: SQL은 이미 작성되어 있었고, `query()` 함수로 교체 완료
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ ExternalDbConnectionService는 **15개의 Prisma 호출**이 있으며, 외부
|
|||
| 항목 | 내용 |
|
||||
| --------------- | ---------------------------------------------------------- |
|
||||
| 파일 위치 | `backend-node/src/services/externalDbConnectionService.ts` |
|
||||
| 파일 크기 | 800+ 라인 |
|
||||
| Prisma 호출 | 15개 |
|
||||
| **현재 진행률** | **0/15 (0%)** ⏳ **진행 예정** |
|
||||
| 파일 크기 | 1,100+ 라인 |
|
||||
| Prisma 호출 | 0개 (전환 완료) |
|
||||
| **현재 진행률** | **15/15 (100%)** ✅ **완료** |
|
||||
| 복잡도 | 중간 (CRUD + 연결 테스트) |
|
||||
| 우선순위 | 🟡 중간 (Phase 2.5) |
|
||||
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
|
||||
|
||||
### 🎯 전환 목표
|
||||
|
||||
|
|
@ -82,18 +83,43 @@ 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 클래스와 통합 유지
|
||||
|
||||
## 🎯 완료 기준
|
||||
|
||||
- [ ] **15개 Prisma 호출 모두 Raw Query로 전환**
|
||||
- [ ] **암호화/복호화 로직 정상 동작**
|
||||
- [ ] **연결 테스트 정상 동작**
|
||||
- [ ] **모든 단위 테스트 통과 (10개 이상)**
|
||||
- [ ] **Prisma import 완전 제거**
|
||||
- [x] **15개 Prisma 호출 모두 Raw Query로 전환** ✅
|
||||
- [x] **암호화/복호화 로직 정상 동작** ✅
|
||||
- [x] **연결 테스트 정상 동작** ✅
|
||||
- [ ] **모든 단위 테스트 통과 (10개 이상)** ⏳
|
||||
- [x] **Prisma import 완전 제거** ✅
|
||||
- [x] **TypeScript 컴파일 성공** ✅
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-09-30
|
||||
**예상 소요 시간**: 1일
|
||||
**완료일**: 2025-10-01
|
||||
**소요 시간**: 1시간
|
||||
**담당자**: 백엔드 개발팀
|
||||
**우선순위**: 🟡 중간 (Phase 2.5)
|
||||
**상태**: ⏳ **진행 예정**
|
||||
**상태**: ✅ **전환 완료** (테스트 필요)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ DataflowControlService는 **6개의 Prisma 호출**이 있으며, 데이터플
|
|||
| 항목 | 내용 |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| 파일 위치 | `backend-node/src/services/dataflowControlService.ts` |
|
||||
| 파일 크기 | 600+ 라인 |
|
||||
| Prisma 호출 | 6개 |
|
||||
| **현재 진행률** | **0/6 (0%)** ⏳ **진행 예정** |
|
||||
| 파일 크기 | 1,100+ 라인 |
|
||||
| Prisma 호출 | 0개 (전환 완료) |
|
||||
| **현재 진행률** | **6/6 (100%)** ✅ **완료** |
|
||||
| 복잡도 | 높음 (복잡한 비즈니스 로직) |
|
||||
| 우선순위 | 🟡 중간 (Phase 2.6) |
|
||||
| **상태** | ✅ **전환 완료 및 컴파일 성공** |
|
||||
|
||||
### 🎯 전환 목표
|
||||
|
||||
|
|
@ -163,16 +164,34 @@ 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. **복잡한 비즈니스 로직 유지**: 조건부 실행, 다중 커넥션, 에러 처리
|
||||
|
||||
## 🎯 완료 기준
|
||||
|
||||
- [ ] **6개 모든 Prisma 호출을 Raw Query로 전환 완료**
|
||||
- [ ] **모든 TypeScript 컴파일 오류 해결**
|
||||
- [ ] **트랜잭션 정상 동작 확인**
|
||||
- [ ] **복잡한 비즈니스 로직 정상 동작**
|
||||
- [ ] **모든 단위 테스트 통과 (10개)**
|
||||
- [ ] **모든 통합 테스트 작성 완료 (4개 시나리오)**
|
||||
- [ ] **`import prisma` 완전 제거 및 `import { query, transaction } from "../database/db"` 사용**
|
||||
- [ ] **성능 저하 없음**
|
||||
- [x] **6개 모든 Prisma 호출을 Raw Query로 전환 완료** ✅
|
||||
- [x] **모든 TypeScript 컴파일 오류 해결** ✅
|
||||
- [x] **`import prisma` 완전 제거** ✅
|
||||
- [ ] **트랜잭션 정상 동작 확인** ⏳
|
||||
- [ ] **복잡한 비즈니스 로직 정상 동작** ⏳
|
||||
- [ ] **모든 단위 테스트 통과 (10개)** ⏳
|
||||
- [ ] **모든 통합 테스트 작성 완료 (4개 시나리오)** ⏳
|
||||
- [ ] **성능 저하 없음** ⏳
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -198,8 +217,9 @@ await transaction(async (client) => {
|
|||
---
|
||||
|
||||
**작성일**: 2025-09-30
|
||||
**예상 소요 시간**: 1일
|
||||
**완료일**: 2025-10-01
|
||||
**소요 시간**: 30분
|
||||
**담당자**: 백엔드 개발팀
|
||||
**우선순위**: 🟡 중간 (Phase 2.6)
|
||||
**상태**: ⏳ **진행 예정**
|
||||
**상태**: ✅ **전환 완료** (테스트 필요)
|
||||
**특이사항**: 복잡한 비즈니스 로직이 포함되어 있어 신중한 테스트 필요
|
||||
|
|
|
|||
|
|
@ -0,0 +1,369 @@
|
|||
# 🎨 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 쿼리 포함
|
||||
|
||||
|
|
@ -0,0 +1,484 @@
|
|||
# 🗂️ 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 포함
|
||||
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
# 📋 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 쿼리 포함
|
||||
|
||||
|
|
@ -28,9 +28,10 @@ backend-node/src/services/
|
|||
├── screenManagementService.ts # 화면 관리 (46개 호출) ⭐ 최우선
|
||||
├── tableManagementService.ts # 테이블 관리 (35개 호출) ⭐ 최우선
|
||||
├── dataflowService.ts # 데이터플로우 (0개 호출) ✅ 전환 완료
|
||||
├── dynamicFormService.ts # 동적 폼 (15개 호출)
|
||||
├── externalDbConnectionService.ts # 외부DB (15개 호출)
|
||||
├── dataflowControlService.ts # 제어관리 (6개 호출)
|
||||
├── dynamicFormService.ts # 동적 폼 (0개 호출) ✅ 전환 완료
|
||||
├── externalDbConnectionService.ts # 외부DB (0개 호출) ✅ 전환 완료
|
||||
├── dataflowControlService.ts # 제어관리 (0개 호출) ✅ 전환 완료
|
||||
├── multilangService.ts # 다국어 (0개 호출) ✅ 전환 완료
|
||||
├── ddlExecutionService.ts # DDL 실행 (6개 호출)
|
||||
├── authService.ts # 인증 (5개 호출)
|
||||
└── multiConnectionQueryService.ts # 다중 연결 (4개 호출)
|
||||
|
|
@ -113,21 +114,21 @@ backend-node/ (루트)
|
|||
- `screenManagementService.ts` (46개) - 화면 정의 관리, JSON 처리
|
||||
- `tableManagementService.ts` (35개) - 테이블 메타데이터 관리, DDL 실행
|
||||
- `dataflowService.ts` (0개) - ✅ **전환 완료** (Phase 2.3)
|
||||
- `dynamicFormService.ts` (15개) - UPSERT 및 동적 테이블 처리
|
||||
- `externalDbConnectionService.ts` (15개) - 외부 DB 연결 관리
|
||||
- `dataflowControlService.ts` (6개) - 복잡한 제어 로직
|
||||
- `dynamicFormService.ts` (0개) - ✅ **전환 완료** (Phase 2.4)
|
||||
- `externalDbConnectionService.ts` (0개) - ✅ **전환 완료** (Phase 2.5)
|
||||
- `dataflowControlService.ts` (0개) - ✅ **전환 완료** (Phase 2.6)
|
||||
- `enhancedDataflowControlService.ts` (0개) - 다중 연결 제어 (Raw Query만 사용)
|
||||
- `multiConnectionQueryService.ts` (4개) - 외부 DB 연결
|
||||
|
||||
#### 🟠 **복잡 (Raw Query 혼재) - 2순위**
|
||||
|
||||
- `multilangService.ts` (25개) - 재귀 쿼리, 다국어 처리
|
||||
- `batchService.ts` (16개) - 배치 작업 관리
|
||||
- `componentStandardService.ts` (16개) - 컴포넌트 표준 관리
|
||||
- `commonCodeService.ts` (15개) - 코드 관리, 계층 구조
|
||||
- `dataflowDiagramService.ts` (12개) - 다이어그램 관리 ⭐ 신규 발견
|
||||
- `collectionService.ts` (11개) - 컬렉션 관리
|
||||
- `layoutService.ts` (10개) - 레이아웃 관리
|
||||
- `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)
|
||||
- `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견
|
||||
- `templateStandardService.ts` (9개) - 템플릿 표준
|
||||
- `eventTriggerService.ts` (6개) - JSON 검색 쿼리
|
||||
|
|
@ -1099,15 +1100,26 @@ describe("Performance Benchmarks", () => {
|
|||
|
||||
#### ⏳ 진행 예정 서비스
|
||||
|
||||
- [ ] **DynamicFormService 전환 (13개)** - Phase 2.4 🟢 낮은 우선순위
|
||||
- 13개 Prisma 호출 ($queryRaw 11개 + ORM 2개)
|
||||
- SQL은 85% 작성 완료 → `query()` 함수로 교체만 필요
|
||||
- [x] **DynamicFormService 전환 (13개)** ✅ **완료** (Phase 2.4)
|
||||
- [x] 13개 Prisma 호출 전환 완료 (동적 폼 CRUD + UPSERT)
|
||||
- [x] 동적 UPSERT 쿼리 구현 (ON CONFLICT 구문)
|
||||
- [x] 부분 업데이트 및 타입 변환 로직 유지
|
||||
- [x] TypeScript 컴파일 성공
|
||||
- [x] Prisma import 완전 제거
|
||||
- 📄 **[PHASE2.4_DYNAMIC_FORM_MIGRATION.md](PHASE2.4_DYNAMIC_FORM_MIGRATION.md)**
|
||||
- [ ] **ExternalDbConnectionService 전환 (15개)** - Phase 2.6 🟡 중간 우선순위
|
||||
- 15개 Prisma 호출 (외부 DB 연결 관리)
|
||||
- [x] **ExternalDbConnectionService 전환 (15개)** ✅ **완료** (Phase 2.5)
|
||||
- [x] 15개 Prisma 호출 전환 완료 (외부 DB 연결 CRUD + 테스트)
|
||||
- [x] 동적 WHERE 조건 생성 및 동적 UPDATE 쿼리 구현
|
||||
- [x] 암호화/복호화 로직 유지
|
||||
- [x] TypeScript 컴파일 성공
|
||||
- [x] Prisma import 완전 제거
|
||||
- 📄 **[PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md](PHASE2.5_EXTERNAL_DB_CONNECTION_MIGRATION.md)**
|
||||
- [ ] **DataflowControlService 전환 (6개)** - Phase 2.7 🟡 중간 우선순위
|
||||
- 6개 Prisma 호출 (복잡한 비즈니스 로직)
|
||||
- [x] **DataflowControlService 전환 (6개)** ✅ **완료** (Phase 2.6)
|
||||
- [x] 6개 Prisma 호출 전환 완료 (데이터플로우 제어 + 동적 테이블 CRUD)
|
||||
- [x] 파라미터 바인딩 수정 (MySQL → PostgreSQL 스타일)
|
||||
- [x] 복잡한 비즈니스 로직 유지
|
||||
- [x] TypeScript 컴파일 성공
|
||||
- [x] Prisma import 완전 제거
|
||||
- 📄 **[PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md](PHASE2.6_DATAFLOW_CONTROL_MIGRATION.md)**
|
||||
|
||||
#### ✅ 다른 Phase로 이동
|
||||
|
|
@ -1117,19 +1129,82 @@ describe("Performance Benchmarks", () => {
|
|||
|
||||
### **Phase 3: 관리 기능 (2.5주) - 162개 호출**
|
||||
|
||||
- [ ] MultiLangService 전환 (25개) - 재귀 쿼리
|
||||
- [ ] 배치 관련 서비스 전환 (40개) ⭐ 대규모 신규 발견
|
||||
- [ ] BatchService (16개), BatchExternalDbService (8개)
|
||||
- [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개)
|
||||
- [ ] BatchExecutionLogService (7개), BatchManagementService (5개)
|
||||
- [ ] BatchSchedulerService (4개)
|
||||
- [ ] 표준 관리 서비스 전환 (41개)
|
||||
- [ ] ComponentStandardService (16개), CommonCodeService (15개)
|
||||
- [ ] LayoutService (10개)
|
||||
- [ ] 데이터플로우 관련 서비스 (18개) ⭐ 신규 발견
|
||||
- [ ] DataflowDiagramService (12개), DataflowControlService (6개)
|
||||
- [ ] 기타 중요 서비스 (38개) ⭐ 신규 발견
|
||||
- [ ] CollectionService (11개), DbTypeCategoryService (10개)
|
||||
- [ ] TemplateStandardService (9개), DDLAuditLogger (8개)
|
||||
- [ ] 표준 관리 서비스 전환 (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개)
|
||||
- [ ] 기능별 테스트 완료
|
||||
|
||||
### **Phase 4: 확장 기능 (2.5주) - 129개 호출 ⭐ 대폭 확장**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
|||
// 수집 관리 서비스
|
||||
// 작성일: 2024-12-23
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import {
|
||||
DataCollectionConfig,
|
||||
CollectionFilter,
|
||||
|
|
@ -9,8 +9,6 @@ import {
|
|||
CollectionHistory,
|
||||
} from "../types/collectionManagement";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class CollectionService {
|
||||
/**
|
||||
* 수집 설정 목록 조회
|
||||
|
|
@ -18,40 +16,44 @@ export class CollectionService {
|
|||
static async getCollectionConfigs(
|
||||
filter: CollectionFilter
|
||||
): Promise<DataCollectionConfig[]> {
|
||||
const whereCondition: any = {
|
||||
company_code: filter.company_code || "*",
|
||||
};
|
||||
const whereConditions: string[] = ["company_code = $1"];
|
||||
const values: any[] = [filter.company_code || "*"];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (filter.config_name) {
|
||||
whereCondition.config_name = {
|
||||
contains: filter.config_name,
|
||||
mode: "insensitive",
|
||||
};
|
||||
whereConditions.push(`config_name ILIKE $${paramIndex++}`);
|
||||
values.push(`%${filter.config_name}%`);
|
||||
}
|
||||
|
||||
if (filter.source_connection_id) {
|
||||
whereCondition.source_connection_id = filter.source_connection_id;
|
||||
whereConditions.push(`source_connection_id = $${paramIndex++}`);
|
||||
values.push(filter.source_connection_id);
|
||||
}
|
||||
|
||||
if (filter.collection_type) {
|
||||
whereCondition.collection_type = filter.collection_type;
|
||||
whereConditions.push(`collection_type = $${paramIndex++}`);
|
||||
values.push(filter.collection_type);
|
||||
}
|
||||
|
||||
if (filter.is_active) {
|
||||
whereCondition.is_active = filter.is_active === "Y";
|
||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
||||
values.push(filter.is_active === "Y");
|
||||
}
|
||||
|
||||
if (filter.search) {
|
||||
whereCondition.OR = [
|
||||
{ config_name: { contains: filter.search, mode: "insensitive" } },
|
||||
{ description: { contains: filter.search, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(config_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${filter.search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const configs = await prisma.data_collection_configs.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: { created_date: "desc" },
|
||||
});
|
||||
const configs = await query<any>(
|
||||
`SELECT * FROM data_collection_configs
|
||||
WHERE ${whereConditions.join(" AND ")}
|
||||
ORDER BY created_date DESC`,
|
||||
values
|
||||
);
|
||||
|
||||
return configs.map((config: any) => ({
|
||||
...config,
|
||||
|
|
@ -65,9 +67,10 @@ export class CollectionService {
|
|||
static async getCollectionConfigById(
|
||||
id: number
|
||||
): Promise<DataCollectionConfig | null> {
|
||||
const config = await prisma.data_collection_configs.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const config = await queryOne<any>(
|
||||
`SELECT * FROM data_collection_configs WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
|
|
@ -84,15 +87,26 @@ export class CollectionService {
|
|||
data: DataCollectionConfig
|
||||
): Promise<DataCollectionConfig> {
|
||||
const { id, collection_options, ...createData } = data;
|
||||
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(),
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
...config,
|
||||
|
|
@ -107,19 +121,52 @@ export class CollectionService {
|
|||
id: number,
|
||||
data: Partial<DataCollectionConfig>
|
||||
): Promise<DataCollectionConfig> {
|
||||
const updateData: any = {
|
||||
...data,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
const updateFields: string[] = ["updated_date = NOW()"];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
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) {
|
||||
updateData.is_active = data.is_active;
|
||||
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);
|
||||
}
|
||||
|
||||
const config = await prisma.data_collection_configs.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
const config = await queryOne<any>(
|
||||
`UPDATE data_collection_configs
|
||||
SET ${updateFields.join(", ")}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *`,
|
||||
[...values, id]
|
||||
);
|
||||
|
||||
return {
|
||||
...config,
|
||||
|
|
@ -131,18 +178,17 @@ export class CollectionService {
|
|||
* 수집 설정 삭제
|
||||
*/
|
||||
static async deleteCollectionConfig(id: number): Promise<void> {
|
||||
await prisma.data_collection_configs.delete({
|
||||
where: { id },
|
||||
});
|
||||
await query(`DELETE FROM data_collection_configs WHERE id = $1`, [id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수집 작업 실행
|
||||
*/
|
||||
static async executeCollection(configId: number): Promise<CollectionJob> {
|
||||
const config = await prisma.data_collection_configs.findUnique({
|
||||
where: { id: configId },
|
||||
});
|
||||
const config = await queryOne<any>(
|
||||
`SELECT * FROM data_collection_configs WHERE id = $1`,
|
||||
[configId]
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
throw new Error("수집 설정을 찾을 수 없습니다.");
|
||||
|
|
@ -153,14 +199,13 @@ export class CollectionService {
|
|||
}
|
||||
|
||||
// 수집 작업 기록 생성
|
||||
const job = await prisma.data_collection_jobs.create({
|
||||
data: {
|
||||
config_id: configId,
|
||||
job_status: "running",
|
||||
started_at: new Date(),
|
||||
created_date: new Date(),
|
||||
},
|
||||
});
|
||||
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"]
|
||||
);
|
||||
|
||||
// 실제 수집 작업 실행 로직은 여기에 구현
|
||||
// 현재는 시뮬레이션으로 처리
|
||||
|
|
@ -171,24 +216,23 @@ export class CollectionService {
|
|||
|
||||
const recordsCollected = Math.floor(Math.random() * 1000) + 100;
|
||||
|
||||
await prisma.data_collection_jobs.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
job_status: "completed",
|
||||
completed_at: new Date(),
|
||||
records_processed: recordsCollected,
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE data_collection_jobs
|
||||
SET job_status = $1, completed_at = NOW(), records_processed = $2
|
||||
WHERE id = $3`,
|
||||
["completed", recordsCollected, job.id]
|
||||
);
|
||||
} catch (error) {
|
||||
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 : "알 수 없는 오류",
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
|
@ -199,24 +243,21 @@ export class CollectionService {
|
|||
* 수집 작업 목록 조회
|
||||
*/
|
||||
static async getCollectionJobs(configId?: number): Promise<CollectionJob[]> {
|
||||
const whereCondition: any = {};
|
||||
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[] = [];
|
||||
|
||||
if (configId) {
|
||||
whereCondition.config_id = configId;
|
||||
sql += ` WHERE j.config_id = $1`;
|
||||
values.push(configId);
|
||||
}
|
||||
|
||||
const jobs = await prisma.data_collection_jobs.findMany({
|
||||
where: whereCondition,
|
||||
orderBy: { started_at: "desc" },
|
||||
include: {
|
||||
config: {
|
||||
select: {
|
||||
config_name: true,
|
||||
collection_type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
sql += ` ORDER BY j.started_at DESC`;
|
||||
|
||||
const jobs = await query<any>(sql, values);
|
||||
|
||||
return jobs as CollectionJob[];
|
||||
}
|
||||
|
|
@ -227,11 +268,13 @@ export class CollectionService {
|
|||
static async getCollectionHistory(
|
||||
configId: number
|
||||
): Promise<CollectionHistory[]> {
|
||||
const history = await prisma.data_collection_jobs.findMany({
|
||||
where: { config_id: configId },
|
||||
orderBy: { started_at: "desc" },
|
||||
take: 50, // 최근 50개 이력
|
||||
});
|
||||
const history = await query<any>(
|
||||
`SELECT * FROM data_collection_jobs
|
||||
WHERE config_id = $1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 50`,
|
||||
[configId]
|
||||
);
|
||||
|
||||
return history.map((item: any) => ({
|
||||
id: item.id,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export interface CodeCategory {
|
||||
|
|
@ -69,30 +68,46 @@ export class CommonCodeService {
|
|||
try {
|
||||
const { search, isActive, page = 1, size = 20 } = params;
|
||||
|
||||
let whereClause: any = {};
|
||||
const whereConditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (search) {
|
||||
whereClause.OR = [
|
||||
{ category_name: { contains: search, mode: "insensitive" } },
|
||||
{ category_code: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(category_name ILIKE $${paramIndex} OR category_code ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (isActive !== undefined) {
|
||||
whereClause.is_active = isActive ? "Y" : "N";
|
||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
||||
values.push(isActive ? "Y" : "N");
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
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 }),
|
||||
]);
|
||||
// 카테고리 조회
|
||||
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");
|
||||
|
||||
logger.info(
|
||||
`카테고리 조회 완료: ${categories.length}개, 전체: ${total}개`
|
||||
|
|
@ -115,32 +130,43 @@ export class CommonCodeService {
|
|||
try {
|
||||
const { search, isActive, page = 1, size = 20 } = params;
|
||||
|
||||
let whereClause: any = {
|
||||
code_category: categoryCode,
|
||||
};
|
||||
const whereConditions: string[] = ["code_category = $1"];
|
||||
const values: any[] = [categoryCode];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (search) {
|
||||
whereClause.OR = [
|
||||
{ code_name: { contains: search, mode: "insensitive" } },
|
||||
{ code_value: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(code_name ILIKE $${paramIndex} OR code_value ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (isActive !== undefined) {
|
||||
whereClause.is_active = isActive ? "Y" : "N";
|
||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
||||
values.push(isActive ? "Y" : "N");
|
||||
}
|
||||
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
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 }),
|
||||
]);
|
||||
// 코드 조회
|
||||
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");
|
||||
|
||||
logger.info(
|
||||
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}개`
|
||||
|
|
@ -158,18 +184,22 @@ export class CommonCodeService {
|
|||
*/
|
||||
async createCategory(data: CreateCategoryData, createdBy: string) {
|
||||
try {
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(`카테고리 생성 완료: ${data.categoryCode}`);
|
||||
return category;
|
||||
|
|
@ -190,23 +220,49 @@ export class CommonCodeService {
|
|||
try {
|
||||
// 디버깅: 받은 데이터 로그
|
||||
logger.info(`카테고리 수정 데이터:`, { categoryCode, data });
|
||||
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(),
|
||||
},
|
||||
});
|
||||
|
||||
// 동적 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]
|
||||
);
|
||||
|
||||
logger.info(`카테고리 수정 완료: ${categoryCode}`);
|
||||
return category;
|
||||
|
|
@ -221,9 +277,9 @@ export class CommonCodeService {
|
|||
*/
|
||||
async deleteCategory(categoryCode: string) {
|
||||
try {
|
||||
await prisma.code_category.delete({
|
||||
where: { category_code: categoryCode },
|
||||
});
|
||||
await query(`DELETE FROM code_category WHERE category_code = $1`, [
|
||||
categoryCode,
|
||||
]);
|
||||
|
||||
logger.info(`카테고리 삭제 완료: ${categoryCode}`);
|
||||
} catch (error) {
|
||||
|
|
@ -241,19 +297,23 @@ export class CommonCodeService {
|
|||
createdBy: string
|
||||
) {
|
||||
try {
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
logger.info(`코드 생성 완료: ${categoryCode}.${data.codeValue}`);
|
||||
return code;
|
||||
|
|
@ -278,28 +338,49 @@ export class CommonCodeService {
|
|||
try {
|
||||
// 디버깅: 받은 데이터 로그
|
||||
logger.info(`코드 수정 데이터:`, { categoryCode, codeValue, data });
|
||||
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(),
|
||||
},
|
||||
});
|
||||
|
||||
// 동적 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]
|
||||
);
|
||||
|
||||
logger.info(`코드 수정 완료: ${categoryCode}.${codeValue}`);
|
||||
return code;
|
||||
|
|
@ -314,14 +395,10 @@ export class CommonCodeService {
|
|||
*/
|
||||
async deleteCode(categoryCode: string, codeValue: string) {
|
||||
try {
|
||||
await prisma.code_info.delete({
|
||||
where: {
|
||||
code_category_code_value: {
|
||||
code_category: categoryCode,
|
||||
code_value: codeValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`,
|
||||
[categoryCode, codeValue]
|
||||
);
|
||||
|
||||
logger.info(`코드 삭제 완료: ${categoryCode}.${codeValue}`);
|
||||
} catch (error) {
|
||||
|
|
@ -335,19 +412,18 @@ export class CommonCodeService {
|
|||
*/
|
||||
async getCodeOptions(categoryCode: string) {
|
||||
try {
|
||||
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 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 options = codes.map((code) => ({
|
||||
value: code.code_value,
|
||||
|
|
@ -373,13 +449,14 @@ export class CommonCodeService {
|
|||
) {
|
||||
try {
|
||||
// 먼저 존재하는 코드들을 확인
|
||||
const existingCodes = await prisma.code_info.findMany({
|
||||
where: {
|
||||
code_category: categoryCode,
|
||||
code_value: { in: codes.map((c) => c.codeValue) },
|
||||
},
|
||||
select: { code_value: true },
|
||||
});
|
||||
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 existingCodeValues = existingCodes.map((c) => c.code_value);
|
||||
const validCodes = codes.filter((c) =>
|
||||
|
|
@ -392,23 +469,17 @@ export class CommonCodeService {
|
|||
);
|
||||
}
|
||||
|
||||
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);
|
||||
// 트랜잭션으로 업데이트
|
||||
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 skippedCodes = codes.filter(
|
||||
(c) => !existingCodeValues.includes(c.codeValue)
|
||||
|
|
@ -460,18 +531,38 @@ export class CommonCodeService {
|
|||
break;
|
||||
}
|
||||
|
||||
// 수정 시 자기 자신 제외
|
||||
if (excludeCategoryCode) {
|
||||
whereCondition.category_code = {
|
||||
...whereCondition.category_code,
|
||||
not: excludeCategoryCode,
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
|
||||
const existingCategory = await prisma.code_category.findFirst({
|
||||
where: whereCondition,
|
||||
select: { category_code: true },
|
||||
});
|
||||
// 수정 시 자기 자신 제외
|
||||
if (excludeCategoryCode) {
|
||||
sql += ` AND category_code != $${paramIndex++}`;
|
||||
values.push(excludeCategoryCode);
|
||||
}
|
||||
|
||||
sql += ` LIMIT 1`;
|
||||
|
||||
const existingCategory = await queryOne<{ category_code: string }>(
|
||||
sql,
|
||||
values
|
||||
);
|
||||
|
||||
const isDuplicate = !!existingCategory;
|
||||
const fieldNames = {
|
||||
|
|
@ -527,18 +618,36 @@ export class CommonCodeService {
|
|||
break;
|
||||
}
|
||||
|
||||
// 수정 시 자기 자신 제외
|
||||
if (excludeCodeValue) {
|
||||
whereCondition.code_value = {
|
||||
...whereCondition.code_value,
|
||||
not: excludeCodeValue,
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
|
||||
const existingCode = await prisma.code_info.findFirst({
|
||||
where: whereCondition,
|
||||
select: { code_value: true },
|
||||
});
|
||||
// 수정 시 자기 자신 제외
|
||||
if (excludeCodeValue) {
|
||||
sql += ` AND code_value != $${paramIndex++}`;
|
||||
values.push(excludeCodeValue);
|
||||
}
|
||||
|
||||
sql += ` LIMIT 1`;
|
||||
|
||||
const existingCode = await queryOne<{ code_value: string }>(sql, values);
|
||||
|
||||
const isDuplicate = !!existingCode;
|
||||
const fieldNames = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
|
||||
export interface ComponentStandardData {
|
||||
component_code: string;
|
||||
|
|
@ -49,49 +47,78 @@ class ComponentStandardService {
|
|||
offset = 0,
|
||||
} = params;
|
||||
|
||||
const where: any = {};
|
||||
const whereConditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 활성화 상태 필터
|
||||
if (active) {
|
||||
where.is_active = active;
|
||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
||||
values.push(active);
|
||||
}
|
||||
|
||||
// 카테고리 필터
|
||||
if (category && category !== "all") {
|
||||
where.category = category;
|
||||
whereConditions.push(`category = $${paramIndex++}`);
|
||||
values.push(category);
|
||||
}
|
||||
|
||||
// 공개 여부 필터
|
||||
if (is_public) {
|
||||
where.is_public = is_public;
|
||||
whereConditions.push(`is_public = $${paramIndex++}`);
|
||||
values.push(is_public);
|
||||
}
|
||||
|
||||
// 회사별 필터 (공개 컴포넌트 + 해당 회사 컴포넌트)
|
||||
if (company_code) {
|
||||
where.OR = [{ is_public: "Y" }, { company_code }];
|
||||
whereConditions.push(
|
||||
`(is_public = 'Y' OR company_code = $${paramIndex++})`
|
||||
);
|
||||
values.push(company_code);
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
if (search) {
|
||||
where.OR = [
|
||||
...(where.OR || []),
|
||||
{ component_name: { contains: search, mode: "insensitive" } },
|
||||
{ component_name_eng: { contains: search, mode: "insensitive" } },
|
||||
{ description: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(component_name ILIKE $${paramIndex} OR component_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const orderBy: any = {};
|
||||
orderBy[sort] = order;
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
const components = await prisma.component_standards.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
// 정렬 컬럼 검증 (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 total = await prisma.component_standards.count({ where });
|
||||
// 컴포넌트 조회
|
||||
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");
|
||||
|
||||
return {
|
||||
components,
|
||||
|
|
@ -105,9 +132,10 @@ class ComponentStandardService {
|
|||
* 컴포넌트 상세 조회
|
||||
*/
|
||||
async getComponent(component_code: string) {
|
||||
const component = await prisma.component_standards.findUnique({
|
||||
where: { component_code },
|
||||
});
|
||||
const component = await queryOne<any>(
|
||||
`SELECT * FROM component_standards WHERE component_code = $1`,
|
||||
[component_code]
|
||||
);
|
||||
|
||||
if (!component) {
|
||||
throw new Error(`컴포넌트를 찾을 수 없습니다: ${component_code}`);
|
||||
|
|
@ -121,9 +149,10 @@ class ComponentStandardService {
|
|||
*/
|
||||
async createComponent(data: ComponentStandardData) {
|
||||
// 중복 코드 확인
|
||||
const existing = await prisma.component_standards.findUnique({
|
||||
where: { component_code: data.component_code },
|
||||
});
|
||||
const existing = await queryOne<any>(
|
||||
`SELECT * FROM component_standards WHERE component_code = $1`,
|
||||
[data.component_code]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
|
|
@ -138,13 +167,31 @@ class ComponentStandardService {
|
|||
delete (createData as any).active;
|
||||
}
|
||||
|
||||
const component = await prisma.component_standards.create({
|
||||
data: {
|
||||
...createData,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
|
@ -165,13 +212,41 @@ class ComponentStandardService {
|
|||
delete (updateData as any).active;
|
||||
}
|
||||
|
||||
const component = await prisma.component_standards.update({
|
||||
where: { component_code },
|
||||
data: {
|
||||
...updateData,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
// 동적 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]
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
|
@ -182,9 +257,9 @@ class ComponentStandardService {
|
|||
async deleteComponent(component_code: string) {
|
||||
const existing = await this.getComponent(component_code);
|
||||
|
||||
await prisma.component_standards.delete({
|
||||
where: { component_code },
|
||||
});
|
||||
await query(`DELETE FROM component_standards WHERE component_code = $1`, [
|
||||
component_code,
|
||||
]);
|
||||
|
||||
return { message: `컴포넌트가 삭제되었습니다: ${component_code}` };
|
||||
}
|
||||
|
|
@ -195,14 +270,16 @@ class ComponentStandardService {
|
|||
async updateSortOrder(
|
||||
updates: Array<{ component_code: string; sort_order: number }>
|
||||
) {
|
||||
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);
|
||||
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]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return { message: "정렬 순서가 업데이트되었습니다." };
|
||||
}
|
||||
|
|
@ -218,33 +295,38 @@ class ComponentStandardService {
|
|||
const source = await this.getComponent(source_code);
|
||||
|
||||
// 새 코드 중복 확인
|
||||
const existing = await prisma.component_standards.findUnique({
|
||||
where: { component_code: new_code },
|
||||
});
|
||||
const existing = await queryOne<any>(
|
||||
`SELECT * FROM component_standards WHERE component_code = $1`,
|
||||
[new_code]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
throw new Error(`이미 존재하는 컴포넌트 코드입니다: ${new_code}`);
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
});
|
||||
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",
|
||||
]
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
|
@ -253,19 +335,20 @@ class ComponentStandardService {
|
|||
* 카테고리 목록 조회
|
||||
*/
|
||||
async getCategories(company_code?: string) {
|
||||
const where: any = {
|
||||
is_active: "Y",
|
||||
};
|
||||
const whereConditions: string[] = ["is_active = 'Y'"];
|
||||
const values: any[] = [];
|
||||
|
||||
if (company_code) {
|
||||
where.OR = [{ is_public: "Y" }, { company_code }];
|
||||
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
|
||||
values.push(company_code);
|
||||
}
|
||||
|
||||
const categories = await prisma.component_standards.findMany({
|
||||
where,
|
||||
select: { category: true },
|
||||
distinct: ["category"],
|
||||
});
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const categories = await query<{ category: string }>(
|
||||
`SELECT DISTINCT category FROM component_standards ${whereClause} ORDER BY category`,
|
||||
values
|
||||
);
|
||||
|
||||
return categories
|
||||
.map((item) => item.category)
|
||||
|
|
@ -276,36 +359,48 @@ class ComponentStandardService {
|
|||
* 컴포넌트 통계
|
||||
*/
|
||||
async getStatistics(company_code?: string) {
|
||||
const where: any = {
|
||||
is_active: "Y",
|
||||
};
|
||||
const whereConditions: string[] = ["is_active = 'Y'"];
|
||||
const values: any[] = [];
|
||||
|
||||
if (company_code) {
|
||||
where.OR = [{ is_public: "Y" }, { company_code }];
|
||||
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
|
||||
values.push(company_code);
|
||||
}
|
||||
|
||||
const total = await prisma.component_standards.count({ where });
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const byCategory = await prisma.component_standards.groupBy({
|
||||
by: ["category"],
|
||||
where,
|
||||
_count: { category: true },
|
||||
});
|
||||
// 전체 개수
|
||||
const totalResult = await queryOne<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM component_standards ${whereClause}`,
|
||||
values
|
||||
);
|
||||
const total = parseInt(totalResult?.count || "0");
|
||||
|
||||
const byStatus = await prisma.component_standards.groupBy({
|
||||
by: ["is_active"],
|
||||
_count: { is_active: 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`
|
||||
);
|
||||
|
||||
return {
|
||||
total,
|
||||
byCategory: byCategory.map((item) => ({
|
||||
category: item.category,
|
||||
count: item._count.category,
|
||||
count: parseInt(item.count),
|
||||
})),
|
||||
byStatus: byStatus.map((item) => ({
|
||||
status: item.is_active,
|
||||
count: item._count.is_active,
|
||||
count: parseInt(item.count),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -317,16 +412,21 @@ class ComponentStandardService {
|
|||
component_code: string,
|
||||
company_code?: string
|
||||
): Promise<boolean> {
|
||||
const whereClause: any = { component_code };
|
||||
const whereConditions: string[] = ["component_code = $1"];
|
||||
const values: any[] = [component_code];
|
||||
|
||||
// 회사 코드가 있고 "*"가 아닌 경우에만 조건 추가
|
||||
if (company_code && company_code !== "*") {
|
||||
whereClause.company_code = company_code;
|
||||
whereConditions.push("company_code = $2");
|
||||
values.push(company_code);
|
||||
}
|
||||
|
||||
const existingComponent = await prisma.component_standards.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const existingComponent = await queryOne<any>(
|
||||
`SELECT * FROM component_standards ${whereClause} LIMIT 1`,
|
||||
values
|
||||
);
|
||||
|
||||
return !!existingComponent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// 🔧 Prisma 클라이언트 중복 생성 방지 - 기존 인스턴스 재사용
|
||||
import prisma = require("../config/database");
|
||||
import { query, queryOne } from "../database/db";
|
||||
|
||||
export interface ControlCondition {
|
||||
id: string;
|
||||
|
|
@ -82,9 +81,10 @@ export class DataflowControlService {
|
|||
});
|
||||
|
||||
// 관계도 정보 조회
|
||||
const diagram = await prisma.dataflow_diagrams.findUnique({
|
||||
where: { diagram_id: diagramId },
|
||||
});
|
||||
const diagram = await queryOne<any>(
|
||||
`SELECT * FROM dataflow_diagrams WHERE diagram_id = $1`,
|
||||
[diagramId]
|
||||
);
|
||||
|
||||
if (!diagram) {
|
||||
return {
|
||||
|
|
@ -527,9 +527,9 @@ export class DataflowControlService {
|
|||
}
|
||||
|
||||
// 대상 테이블에서 조건에 맞는 데이터 조회
|
||||
const queryResult = await prisma.$queryRawUnsafe(
|
||||
const queryResult = await query<Record<string, any>>(
|
||||
`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 result = await prisma.$executeRawUnsafe(
|
||||
`
|
||||
INSERT INTO ${targetTable} (${Object.keys(insertData).join(", ")})
|
||||
VALUES (${Object.keys(insertData)
|
||||
.map(() => "?")
|
||||
.join(", ")})
|
||||
`,
|
||||
...Object.values(insertData)
|
||||
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)
|
||||
);
|
||||
|
||||
results.push({
|
||||
|
|
@ -878,10 +878,7 @@ export class DataflowControlService {
|
|||
);
|
||||
console.log(`📊 쿼리 파라미터:`, allValues);
|
||||
|
||||
const result = await prisma.$executeRawUnsafe(
|
||||
updateQuery,
|
||||
...allValues
|
||||
);
|
||||
const result = await query(updateQuery, allValues);
|
||||
|
||||
console.log(
|
||||
`✅ UPDATE 성공 (${i + 1}/${action.fieldMappings.length}):`,
|
||||
|
|
@ -1033,10 +1030,7 @@ export class DataflowControlService {
|
|||
console.log(`🚀 실행할 쿼리:`, deleteQuery);
|
||||
console.log(`📊 쿼리 파라미터:`, whereValues);
|
||||
|
||||
const result = await prisma.$executeRawUnsafe(
|
||||
deleteQuery,
|
||||
...whereValues
|
||||
);
|
||||
const result = await query(deleteQuery, whereValues);
|
||||
|
||||
console.log(`✅ DELETE 성공:`, {
|
||||
table: tableName,
|
||||
|
|
@ -1089,18 +1083,15 @@ export class DataflowControlService {
|
|||
columnName: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe<Array<{ exists: boolean }>>(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
const result = await query<{ 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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// 타입 정의
|
||||
|
|
@ -43,41 +42,41 @@ export const getDataflowDiagrams = async (
|
|||
try {
|
||||
const offset = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const whereClause: {
|
||||
company_code?: string;
|
||||
diagram_name?: {
|
||||
contains: string;
|
||||
mode: "insensitive";
|
||||
};
|
||||
} = {};
|
||||
const whereConditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
whereClause.diagram_name = {
|
||||
contains: searchTerm,
|
||||
mode: "insensitive",
|
||||
};
|
||||
whereConditions.push(`diagram_name ILIKE $${paramIndex++}`);
|
||||
values.push(`%${searchTerm}%`);
|
||||
}
|
||||
|
||||
const whereClause =
|
||||
whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
|
||||
// 총 개수 조회
|
||||
const total = await prisma.dataflow_diagrams.count({
|
||||
where: whereClause,
|
||||
});
|
||||
const countResult = await queryOne<{ count: string }>(
|
||||
`SELECT COUNT(*) as count FROM dataflow_diagrams ${whereClause}`,
|
||||
values
|
||||
);
|
||||
const total = parseInt(countResult?.count || "0");
|
||||
|
||||
// 데이터 조회
|
||||
const diagrams = await prisma.dataflow_diagrams.findMany({
|
||||
where: whereClause,
|
||||
orderBy: {
|
||||
updated_at: "desc",
|
||||
},
|
||||
skip: offset,
|
||||
take: size,
|
||||
});
|
||||
const diagrams = await query<any>(
|
||||
`SELECT * FROM dataflow_diagrams
|
||||
${whereClause}
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
||||
[...values, size, offset]
|
||||
);
|
||||
|
||||
const totalPages = Math.ceil(total / size);
|
||||
|
||||
|
|
@ -104,21 +103,21 @@ export const getDataflowDiagramById = async (
|
|||
companyCode: string
|
||||
) => {
|
||||
try {
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
const whereConditions: string[] = ["diagram_id = $1"];
|
||||
const values: any[] = [diagramId];
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
whereConditions.push("company_code = $2");
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
const diagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
|
||||
|
||||
const diagram = await queryOne<any>(
|
||||
`SELECT * FROM dataflow_diagrams ${whereClause} LIMIT 1`,
|
||||
values
|
||||
);
|
||||
|
||||
return diagram;
|
||||
} catch (error) {
|
||||
|
|
@ -134,23 +133,24 @@ export const createDataflowDiagram = async (
|
|||
data: CreateDataflowDiagramData
|
||||
) => {
|
||||
try {
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
return newDiagram;
|
||||
} catch (error) {
|
||||
|
|
@ -173,21 +173,18 @@ export const updateDataflowDiagram = async (
|
|||
);
|
||||
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
const whereConditions: string[] = ["diagram_id = $1"];
|
||||
const checkValues: any[] = [diagramId];
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
whereConditions.push("company_code = $2");
|
||||
checkValues.push(companyCode);
|
||||
}
|
||||
|
||||
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
const existingDiagram = await queryOne<any>(
|
||||
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
|
||||
checkValues
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`기존 관계도 조회 결과:`,
|
||||
|
|
@ -201,36 +198,45 @@ export const updateDataflowDiagram = async (
|
|||
return null;
|
||||
}
|
||||
|
||||
// 업데이트 실행
|
||||
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(),
|
||||
},
|
||||
});
|
||||
// 동적 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]
|
||||
);
|
||||
|
||||
return updatedDiagram;
|
||||
} catch (error) {
|
||||
|
|
@ -248,32 +254,27 @@ export const deleteDataflowDiagram = async (
|
|||
) => {
|
||||
try {
|
||||
// 먼저 해당 관계도가 존재하는지 확인
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
const whereConditions: string[] = ["diagram_id = $1"];
|
||||
const values: any[] = [diagramId];
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
whereConditions.push("company_code = $2");
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
const existingDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
const existingDiagram = await queryOne<any>(
|
||||
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
|
||||
values
|
||||
);
|
||||
|
||||
if (!existingDiagram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 삭제 실행
|
||||
await prisma.dataflow_diagrams.delete({
|
||||
where: {
|
||||
diagram_id: diagramId,
|
||||
},
|
||||
});
|
||||
await query(`DELETE FROM dataflow_diagrams WHERE diagram_id = $1`, [
|
||||
diagramId,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -293,21 +294,18 @@ export const copyDataflowDiagram = async (
|
|||
) => {
|
||||
try {
|
||||
// 원본 관계도 조회
|
||||
const whereClause: {
|
||||
diagram_id: number;
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_id: diagramId,
|
||||
};
|
||||
const whereConditions: string[] = ["diagram_id = $1"];
|
||||
const values: any[] = [diagramId];
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
whereClause.company_code = companyCode;
|
||||
whereConditions.push("company_code = $2");
|
||||
values.push(companyCode);
|
||||
}
|
||||
|
||||
const originalDiagram = await prisma.dataflow_diagrams.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
const originalDiagram = await queryOne<any>(
|
||||
`SELECT * FROM dataflow_diagrams WHERE ${whereConditions.join(" AND ")} LIMIT 1`,
|
||||
values
|
||||
);
|
||||
|
||||
if (!originalDiagram) {
|
||||
return null;
|
||||
|
|
@ -325,28 +323,19 @@ export const copyDataflowDiagram = async (
|
|||
: originalDiagram.diagram_name;
|
||||
|
||||
// 같은 패턴의 이름들을 찾아서 가장 큰 번호 찾기
|
||||
const copyWhereClause: {
|
||||
diagram_name: {
|
||||
startsWith: string;
|
||||
};
|
||||
company_code?: string;
|
||||
} = {
|
||||
diagram_name: {
|
||||
startsWith: baseName,
|
||||
},
|
||||
};
|
||||
const copyWhereConditions: string[] = ["diagram_name LIKE $1"];
|
||||
const copyValues: any[] = [`${baseName}%`];
|
||||
|
||||
// company_code가 '*'가 아닌 경우에만 필터링
|
||||
if (companyCode !== "*") {
|
||||
copyWhereClause.company_code = companyCode;
|
||||
copyWhereConditions.push("company_code = $2");
|
||||
copyValues.push(companyCode);
|
||||
}
|
||||
|
||||
const existingCopies = await prisma.dataflow_diagrams.findMany({
|
||||
where: copyWhereClause,
|
||||
select: {
|
||||
diagram_name: true,
|
||||
},
|
||||
});
|
||||
const existingCopies = await query<{ diagram_name: string }>(
|
||||
`SELECT diagram_name FROM dataflow_diagrams
|
||||
WHERE ${copyWhereConditions.join(" AND ")}`,
|
||||
copyValues
|
||||
);
|
||||
|
||||
let maxNumber = 0;
|
||||
existingCopies.forEach((copy) => {
|
||||
|
|
@ -363,19 +352,24 @@ export const copyDataflowDiagram = async (
|
|||
}
|
||||
|
||||
// 새로운 관계도 생성
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
return copiedDiagram;
|
||||
} catch (error) {
|
||||
|
|
@ -390,39 +384,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 prisma.dataflow_diagrams.findMany({
|
||||
where: {
|
||||
company_code: companyCode,
|
||||
},
|
||||
select: {
|
||||
diagram_id: true,
|
||||
diagram_name: true,
|
||||
relationships: true,
|
||||
},
|
||||
orderBy: {
|
||||
updated_at: "desc",
|
||||
},
|
||||
});
|
||||
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 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// 외부 DB 연결 서비스
|
||||
// 작성일: 2024-12-17
|
||||
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import {
|
||||
ExternalDbConnection,
|
||||
ExternalDbConnectionFilter,
|
||||
|
|
@ -20,43 +20,47 @@ export class ExternalDbConnectionService {
|
|||
filter: ExternalDbConnectionFilter
|
||||
): Promise<ApiResponse<ExternalDbConnection[]>> {
|
||||
try {
|
||||
const where: any = {};
|
||||
// WHERE 조건 동적 생성
|
||||
const whereConditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 필터 조건 적용
|
||||
if (filter.db_type) {
|
||||
where.db_type = filter.db_type;
|
||||
whereConditions.push(`db_type = $${paramIndex++}`);
|
||||
params.push(filter.db_type);
|
||||
}
|
||||
|
||||
if (filter.is_active) {
|
||||
where.is_active = filter.is_active;
|
||||
whereConditions.push(`is_active = $${paramIndex++}`);
|
||||
params.push(filter.is_active);
|
||||
}
|
||||
|
||||
if (filter.company_code) {
|
||||
where.company_code = filter.company_code;
|
||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
||||
params.push(filter.company_code);
|
||||
}
|
||||
|
||||
// 검색 조건 적용 (연결명 또는 설명에서 검색)
|
||||
if (filter.search && filter.search.trim()) {
|
||||
where.OR = [
|
||||
{
|
||||
connection_name: {
|
||||
contains: filter.search.trim(),
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: {
|
||||
contains: filter.search.trim(),
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
];
|
||||
whereConditions.push(
|
||||
`(connection_name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
|
||||
);
|
||||
params.push(`%${filter.search.trim()}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const connections = await prisma.external_db_connections.findMany({
|
||||
where,
|
||||
orderBy: [{ is_active: "desc" }, { connection_name: "asc" }],
|
||||
});
|
||||
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 safeConnections = connections.map((conn) => ({
|
||||
|
|
@ -89,26 +93,25 @@ export class ExternalDbConnectionService {
|
|||
try {
|
||||
// 기본 연결 목록 조회
|
||||
const connectionsResult = await this.getConnections(filter);
|
||||
|
||||
|
||||
if (!connectionsResult.success || !connectionsResult.data) {
|
||||
return {
|
||||
success: false,
|
||||
message: "연결 목록 조회에 실패했습니다."
|
||||
message: "연결 목록 조회에 실패했습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
// DB 타입 카테고리 정보 조회
|
||||
const categories = await prisma.db_type_categories.findMany({
|
||||
where: { is_active: true },
|
||||
orderBy: [
|
||||
{ sort_order: 'asc' },
|
||||
{ display_name: 'asc' }
|
||||
]
|
||||
});
|
||||
const categories = await query<any>(
|
||||
`SELECT * FROM db_type_categories
|
||||
WHERE is_active = true
|
||||
ORDER BY sort_order ASC, display_name ASC`,
|
||||
[]
|
||||
);
|
||||
|
||||
// DB 타입별로 그룹화
|
||||
const groupedConnections: Record<string, any> = {};
|
||||
|
||||
|
||||
// 카테고리 정보를 포함한 그룹 초기화
|
||||
categories.forEach((category: any) => {
|
||||
groupedConnections[category.type_code] = {
|
||||
|
|
@ -117,36 +120,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];
|
||||
}
|
||||
|
|
@ -155,14 +158,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 : "알 수 없는 오류",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -174,9 +177,10 @@ export class ExternalDbConnectionService {
|
|||
id: number
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const connection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
|
|
@ -214,9 +218,10 @@ export class ExternalDbConnectionService {
|
|||
id: number
|
||||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const connection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
|
|
@ -257,13 +262,11 @@ export class ExternalDbConnectionService {
|
|||
this.validateConnectionData(data);
|
||||
|
||||
// 연결명 중복 확인
|
||||
const existingConnection = await prisma.external_db_connections.findFirst(
|
||||
{
|
||||
where: {
|
||||
connection_name: data.connection_name,
|
||||
company_code: data.company_code,
|
||||
},
|
||||
}
|
||||
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]
|
||||
);
|
||||
|
||||
if (existingConnection) {
|
||||
|
|
@ -276,30 +279,35 @@ export class ExternalDbConnectionService {
|
|||
// 비밀번호 암호화
|
||||
const encryptedPassword = PasswordEncryption.encrypt(data.password);
|
||||
|
||||
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 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 safeConnection = {
|
||||
|
|
@ -332,10 +340,10 @@ export class ExternalDbConnectionService {
|
|||
): Promise<ApiResponse<ExternalDbConnection>> {
|
||||
try {
|
||||
// 기존 연결 확인
|
||||
const existingConnection =
|
||||
await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const existingConnection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!existingConnection) {
|
||||
return {
|
||||
|
|
@ -346,15 +354,18 @@ export class ExternalDbConnectionService {
|
|||
|
||||
// 연결명 중복 확인 (자신 제외)
|
||||
if (data.connection_name) {
|
||||
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 },
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
if (duplicateConnection) {
|
||||
return {
|
||||
|
|
@ -406,23 +417,59 @@ export class ExternalDbConnectionService {
|
|||
}
|
||||
|
||||
// 업데이트 데이터 준비
|
||||
const updateData: any = {
|
||||
...data,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 비밀번호가 변경된 경우 암호화 (연결 테스트 통과 후)
|
||||
if (data.password && data.password !== "***ENCRYPTED***") {
|
||||
updateData.password = PasswordEncryption.encrypt(data.password);
|
||||
} else {
|
||||
// 비밀번호 필드 제거 (변경하지 않음)
|
||||
delete updateData.password;
|
||||
updates.push(`password = $${paramIndex++}`);
|
||||
updateParams.push(PasswordEncryption.encrypt(data.password));
|
||||
}
|
||||
|
||||
const updatedConnection = await prisma.external_db_connections.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
// 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 safeConnection = {
|
||||
|
|
@ -451,10 +498,10 @@ export class ExternalDbConnectionService {
|
|||
*/
|
||||
static async deleteConnection(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const existingConnection =
|
||||
await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const existingConnection = await queryOne(
|
||||
`SELECT id FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!existingConnection) {
|
||||
return {
|
||||
|
|
@ -464,9 +511,7 @@ export class ExternalDbConnectionService {
|
|||
}
|
||||
|
||||
// 물리 삭제 (실제 데이터 삭제)
|
||||
await prisma.external_db_connections.delete({
|
||||
where: { id },
|
||||
});
|
||||
await query(`DELETE FROM external_db_connections WHERE id = $1`, [id]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -491,9 +536,10 @@ export class ExternalDbConnectionService {
|
|||
): Promise<import("../types/externalDbTypes").ConnectionTestResult> {
|
||||
try {
|
||||
// 저장된 연결 정보 조회
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const connection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
|
|
@ -674,10 +720,10 @@ export class ExternalDbConnectionService {
|
|||
*/
|
||||
static async getDecryptedPassword(id: number): Promise<string | null> {
|
||||
try {
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
select: { password: true },
|
||||
});
|
||||
const connection = await queryOne<{ password: string }>(
|
||||
`SELECT password FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
return null;
|
||||
|
|
@ -701,9 +747,10 @@ export class ExternalDbConnectionService {
|
|||
try {
|
||||
// 연결 정보 조회
|
||||
console.log("연결 정보 조회 시작:", { id });
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const connection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
console.log("조회된 연결 정보:", connection);
|
||||
|
||||
if (!connection) {
|
||||
|
|
@ -753,14 +800,25 @@ 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: 기본 방식 사용
|
||||
|
|
@ -846,9 +904,10 @@ export class ExternalDbConnectionService {
|
|||
static async getTables(id: number): Promise<ApiResponse<TableInfo[]>> {
|
||||
try {
|
||||
// 연결 정보 조회
|
||||
const connection = await prisma.external_db_connections.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
const connection = await queryOne<any>(
|
||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!connection) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import prisma from "../config/database";
|
||||
import { query, queryOne } from "../database/db";
|
||||
import {
|
||||
CreateLayoutRequest,
|
||||
UpdateLayoutRequest,
|
||||
|
|
@ -77,42 +76,59 @@ export class LayoutService {
|
|||
|
||||
const skip = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const where: any = {
|
||||
is_active: "Y",
|
||||
OR: [
|
||||
{ company_code: companyCode },
|
||||
...(includePublic ? [{ is_public: "Y" }] : []),
|
||||
],
|
||||
};
|
||||
// 동적 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);
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
whereConditions.push(`category = $${paramIndex++}`);
|
||||
values.push(category);
|
||||
}
|
||||
|
||||
if (layoutType) {
|
||||
where.layout_type = layoutType;
|
||||
whereConditions.push(`layout_type = $${paramIndex++}`);
|
||||
values.push(layoutType);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
where.OR = [
|
||||
...where.OR,
|
||||
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ layout_name_eng: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ description: { contains: searchTerm, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(layout_name ILIKE $${paramIndex} OR layout_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${searchTerm}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
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 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 total = parseInt(countResult?.count || "0");
|
||||
|
||||
return {
|
||||
data: data.map(
|
||||
(layout) =>
|
||||
|
|
@ -149,13 +165,13 @@ export class LayoutService {
|
|||
layoutCode: string,
|
||||
companyCode: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
const layout = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
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"]
|
||||
);
|
||||
|
||||
if (!layout) return null;
|
||||
|
||||
|
|
@ -196,24 +212,31 @@ export class LayoutService {
|
|||
companyCode
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
]
|
||||
);
|
||||
|
||||
return this.mapToLayoutStandard(layout);
|
||||
}
|
||||
|
|
@ -227,47 +250,69 @@ export class LayoutService {
|
|||
userId: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
// 수정 권한 확인
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: request.layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
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"]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
// 동적 UPDATE 쿼리 생성
|
||||
const updateFields: string[] = ["updated_by = $1", "updated_date = NOW()"];
|
||||
const values: any[] = [userId];
|
||||
let paramIndex = 2;
|
||||
|
||||
// 수정할 필드만 업데이트
|
||||
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";
|
||||
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");
|
||||
}
|
||||
|
||||
const updated = await prisma.layout_standards.update({
|
||||
where: { layout_code: request.layoutCode },
|
||||
data: updateData,
|
||||
});
|
||||
const updated = await queryOne<any>(
|
||||
`UPDATE layout_standards
|
||||
SET ${updateFields.join(", ")}
|
||||
WHERE layout_code = $${paramIndex}
|
||||
RETURNING *`,
|
||||
[...values, request.layoutCode]
|
||||
);
|
||||
|
||||
return this.mapToLayoutStandard(updated);
|
||||
}
|
||||
|
|
@ -280,26 +325,22 @@ export class LayoutService {
|
|||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
const existing = await queryOne<any>(
|
||||
`SELECT * FROM layout_standards
|
||||
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
|
||||
[layoutCode, companyCode, "Y"]
|
||||
);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
|
||||
}
|
||||
|
||||
await prisma.layout_standards.update({
|
||||
where: { layout_code: layoutCode },
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE layout_standards
|
||||
SET is_active = $1, updated_by = $2, updated_date = NOW()
|
||||
WHERE layout_code = $3`,
|
||||
["N", userId, layoutCode]
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -342,20 +383,17 @@ export class LayoutService {
|
|||
async getLayoutCountsByCategory(
|
||||
companyCode: string
|
||||
): Promise<Record<string, number>> {
|
||||
const counts = await prisma.layout_standards.groupBy({
|
||||
by: ["category"],
|
||||
_count: {
|
||||
layout_code: true,
|
||||
},
|
||||
where: {
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
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"]
|
||||
);
|
||||
|
||||
return counts.reduce(
|
||||
(acc: Record<string, number>, item: any) => {
|
||||
acc[item.category] = item._count.layout_code;
|
||||
acc[item.category] = parseInt(item.count);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
|
|
@ -370,16 +408,11 @@ export class LayoutService {
|
|||
companyCode: string
|
||||
): Promise<string> {
|
||||
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
|
||||
const existingCodes = await prisma.layout_standards.findMany({
|
||||
where: {
|
||||
layout_code: {
|
||||
startsWith: prefix,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
layout_code: true,
|
||||
},
|
||||
});
|
||||
const existingCodes = await query<{ layout_code: string }>(
|
||||
`SELECT layout_code FROM layout_standards
|
||||
WHERE layout_code LIKE $1`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
|
||||
const maxNumber = existingCodes.reduce((max: number, item: any) => {
|
||||
const match = item.layout_code.match(/_(\d+)$/);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
import { query, queryOne, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import {
|
||||
Language,
|
||||
|
|
@ -15,8 +15,6 @@ import {
|
|||
ApiResponse,
|
||||
} from "../types/multilang";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export class MultiLangService {
|
||||
constructor() {}
|
||||
|
||||
|
|
@ -27,25 +25,27 @@ export class MultiLangService {
|
|||
try {
|
||||
logger.info("언어 목록 조회 시작");
|
||||
|
||||
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 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 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,9 +72,10 @@ export class MultiLangService {
|
|||
logger.info("언어 생성 시작", { languageData });
|
||||
|
||||
// 중복 체크
|
||||
const existingLanguage = await prisma.language_master.findUnique({
|
||||
where: { lang_code: languageData.langCode },
|
||||
});
|
||||
const existingLanguage = await queryOne<{ lang_code: string }>(
|
||||
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
|
||||
[languageData.langCode]
|
||||
);
|
||||
|
||||
if (existingLanguage) {
|
||||
throw new Error(
|
||||
|
|
@ -83,30 +84,44 @@ export class MultiLangService {
|
|||
}
|
||||
|
||||
// 언어 생성
|
||||
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",
|
||||
},
|
||||
});
|
||||
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",
|
||||
]
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
@ -127,42 +142,72 @@ export class MultiLangService {
|
|||
logger.info("언어 수정 시작", { langCode, languageData });
|
||||
|
||||
// 기존 언어 확인
|
||||
const existingLanguage = await prisma.language_master.findUnique({
|
||||
where: { lang_code: langCode },
|
||||
});
|
||||
const existingLanguage = await queryOne<{ lang_code: string }>(
|
||||
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
|
||||
[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 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",
|
||||
},
|
||||
});
|
||||
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
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
@ -180,10 +225,10 @@ export class MultiLangService {
|
|||
logger.info("언어 상태 토글 시작", { langCode });
|
||||
|
||||
// 현재 언어 조회
|
||||
const currentLanguage = await prisma.language_master.findUnique({
|
||||
where: { lang_code: langCode },
|
||||
select: { is_active: true },
|
||||
});
|
||||
const currentLanguage = await queryOne<{ is_active: string | null }>(
|
||||
`SELECT is_active FROM language_master WHERE lang_code = $1`,
|
||||
[langCode]
|
||||
);
|
||||
|
||||
if (!currentLanguage) {
|
||||
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
|
||||
|
|
@ -192,13 +237,12 @@ export class MultiLangService {
|
|||
const newStatus = currentLanguage.is_active === "Y" ? "N" : "Y";
|
||||
|
||||
// 상태 업데이트
|
||||
await prisma.language_master.update({
|
||||
where: { lang_code: langCode },
|
||||
data: {
|
||||
is_active: newStatus,
|
||||
updated_by: "system",
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE language_master
|
||||
SET is_active = $1, updated_by = $2
|
||||
WHERE lang_code = $3`,
|
||||
[newStatus, "system", langCode]
|
||||
);
|
||||
|
||||
const result = newStatus === "Y" ? "활성화" : "비활성화";
|
||||
logger.info("언어 상태 토글 완료", { langCode, result });
|
||||
|
|
@ -219,47 +263,55 @@ export class MultiLangService {
|
|||
try {
|
||||
logger.info("다국어 키 목록 조회 시작", { params });
|
||||
|
||||
const whereConditions: any = {};
|
||||
const whereConditions: string[] = [];
|
||||
const values: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 회사 코드 필터
|
||||
if (params.companyCode) {
|
||||
whereConditions.company_code = params.companyCode;
|
||||
whereConditions.push(`company_code = $${paramIndex++}`);
|
||||
values.push(params.companyCode);
|
||||
}
|
||||
|
||||
// 메뉴 코드 필터
|
||||
if (params.menuCode) {
|
||||
whereConditions.menu_name = params.menuCode;
|
||||
whereConditions.push(`menu_name = $${paramIndex++}`);
|
||||
values.push(params.menuCode);
|
||||
}
|
||||
|
||||
// 검색 조건
|
||||
// 검색 조건 (OR)
|
||||
if (params.searchText) {
|
||||
whereConditions.OR = [
|
||||
{ lang_key: { contains: params.searchText, mode: "insensitive" } },
|
||||
{ description: { contains: params.searchText, mode: "insensitive" } },
|
||||
{ menu_name: { contains: params.searchText, mode: "insensitive" } },
|
||||
];
|
||||
whereConditions.push(
|
||||
`(lang_key ILIKE $${paramIndex} OR description ILIKE $${paramIndex} OR menu_name ILIKE $${paramIndex})`
|
||||
);
|
||||
values.push(`%${params.searchText}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
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 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 mappedKeys: LangKey[] = langKeys.map((key) => ({
|
||||
keyId: key.key_id,
|
||||
|
|
@ -291,24 +343,24 @@ export class MultiLangService {
|
|||
try {
|
||||
logger.info("다국어 텍스트 조회 시작", { keyId });
|
||||
|
||||
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 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 mappedTexts: LangText[] = langTexts.map((text) => ({
|
||||
textId: text.text_id,
|
||||
|
|
@ -340,12 +392,11 @@ export class MultiLangService {
|
|||
logger.info("다국어 키 생성 시작", { keyData });
|
||||
|
||||
// 중복 체크
|
||||
const existingKey = await prisma.multi_lang_key_master.findFirst({
|
||||
where: {
|
||||
company_code: keyData.companyCode,
|
||||
lang_key: keyData.langKey,
|
||||
},
|
||||
});
|
||||
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]
|
||||
);
|
||||
|
||||
if (existingKey) {
|
||||
throw new Error(
|
||||
|
|
@ -354,24 +405,28 @@ export class MultiLangService {
|
|||
}
|
||||
|
||||
// 다국어 키 생성
|
||||
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",
|
||||
},
|
||||
});
|
||||
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",
|
||||
]
|
||||
);
|
||||
|
||||
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(
|
||||
|
|
@ -391,9 +446,10 @@ export class MultiLangService {
|
|||
logger.info("다국어 키 수정 시작", { keyId, keyData });
|
||||
|
||||
// 기존 키 확인
|
||||
const existingKey = await prisma.multi_lang_key_master.findUnique({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
const existingKey = await queryOne<{ key_id: number }>(
|
||||
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
|
||||
[keyId]
|
||||
);
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
|
||||
|
|
@ -401,13 +457,11 @@ export class MultiLangService {
|
|||
|
||||
// 중복 체크 (자신을 제외하고)
|
||||
if (keyData.companyCode && keyData.langKey) {
|
||||
const duplicateKey = await prisma.multi_lang_key_master.findFirst({
|
||||
where: {
|
||||
company_code: keyData.companyCode,
|
||||
lang_key: keyData.langKey,
|
||||
key_id: { not: keyId },
|
||||
},
|
||||
});
|
||||
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]
|
||||
);
|
||||
|
||||
if (duplicateKey) {
|
||||
throw new Error(
|
||||
|
|
@ -416,21 +470,39 @@ 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 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",
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE multi_lang_key_master SET ${updates.join(", ")}
|
||||
WHERE key_id = $${paramIndex}`,
|
||||
values
|
||||
);
|
||||
|
||||
logger.info("다국어 키 수정 완료", { keyId });
|
||||
} catch (error) {
|
||||
|
|
@ -449,25 +521,27 @@ export class MultiLangService {
|
|||
logger.info("다국어 키 삭제 시작", { keyId });
|
||||
|
||||
// 기존 키 확인
|
||||
const existingKey = await prisma.multi_lang_key_master.findUnique({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
const existingKey = await queryOne<{ key_id: number }>(
|
||||
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
|
||||
[keyId]
|
||||
);
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
|
||||
}
|
||||
|
||||
// 트랜잭션으로 키와 연관된 텍스트 모두 삭제
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await transaction(async (client) => {
|
||||
// 관련된 다국어 텍스트 삭제
|
||||
await tx.multi_lang_text.deleteMany({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [
|
||||
keyId,
|
||||
]);
|
||||
|
||||
// 다국어 키 삭제
|
||||
await tx.multi_lang_key_master.delete({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
await client.query(
|
||||
`DELETE FROM multi_lang_key_master WHERE key_id = $1`,
|
||||
[keyId]
|
||||
);
|
||||
});
|
||||
|
||||
logger.info("다국어 키 삭제 완료", { keyId });
|
||||
|
|
@ -487,10 +561,10 @@ export class MultiLangService {
|
|||
logger.info("다국어 키 상태 토글 시작", { keyId });
|
||||
|
||||
// 현재 키 조회
|
||||
const currentKey = await prisma.multi_lang_key_master.findUnique({
|
||||
where: { key_id: keyId },
|
||||
select: { is_active: true },
|
||||
});
|
||||
const currentKey = await queryOne<{ is_active: string | null }>(
|
||||
`SELECT is_active FROM multi_lang_key_master WHERE key_id = $1`,
|
||||
[keyId]
|
||||
);
|
||||
|
||||
if (!currentKey) {
|
||||
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
|
||||
|
|
@ -499,13 +573,12 @@ export class MultiLangService {
|
|||
const newStatus = currentKey.is_active === "Y" ? "N" : "Y";
|
||||
|
||||
// 상태 업데이트
|
||||
await prisma.multi_lang_key_master.update({
|
||||
where: { key_id: keyId },
|
||||
data: {
|
||||
is_active: newStatus,
|
||||
updated_by: "system",
|
||||
},
|
||||
});
|
||||
await query(
|
||||
`UPDATE multi_lang_key_master
|
||||
SET is_active = $1, updated_by = $2
|
||||
WHERE key_id = $3`,
|
||||
[newStatus, "system", keyId]
|
||||
);
|
||||
|
||||
const result = newStatus === "Y" ? "활성화" : "비활성화";
|
||||
logger.info("다국어 키 상태 토글 완료", { keyId, result });
|
||||
|
|
@ -533,33 +606,39 @@ export class MultiLangService {
|
|||
});
|
||||
|
||||
// 기존 키 확인
|
||||
const existingKey = await prisma.multi_lang_key_master.findUnique({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
const existingKey = await queryOne<{ key_id: number }>(
|
||||
`SELECT key_id FROM multi_lang_key_master WHERE key_id = $1`,
|
||||
[keyId]
|
||||
);
|
||||
|
||||
if (!existingKey) {
|
||||
throw new Error(`다국어 키를 찾을 수 없습니다: ${keyId}`);
|
||||
}
|
||||
|
||||
// 트랜잭션으로 기존 텍스트 삭제 후 새로 생성
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await transaction(async (client) => {
|
||||
// 기존 텍스트 삭제
|
||||
await tx.multi_lang_text.deleteMany({
|
||||
where: { key_id: keyId },
|
||||
});
|
||||
await client.query(`DELETE FROM multi_lang_text WHERE key_id = $1`, [
|
||||
keyId,
|
||||
]);
|
||||
|
||||
// 새로운 텍스트 삽입
|
||||
if (textData.texts.length > 0) {
|
||||
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",
|
||||
})),
|
||||
});
|
||||
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",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -582,21 +661,25 @@ export class MultiLangService {
|
|||
try {
|
||||
logger.info("사용자별 다국어 텍스트 조회 시작", { params });
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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",
|
||||
]
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
logger.warn("사용자별 다국어 텍스트를 찾을 수 없음", { params });
|
||||
|
|
@ -632,20 +715,17 @@ export class MultiLangService {
|
|||
langCode,
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
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"]
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
logger.warn("특정 키의 다국어 텍스트를 찾을 수 없음", {
|
||||
|
|
@ -691,31 +771,26 @@ export class MultiLangService {
|
|||
}
|
||||
|
||||
// 모든 키에 대한 번역 조회
|
||||
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 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 result: Record<string, string> = {};
|
||||
|
||||
|
|
@ -726,7 +801,7 @@ export class MultiLangService {
|
|||
|
||||
// 실제 번역으로 덮어쓰기 (회사별 우선)
|
||||
translations.forEach((translation) => {
|
||||
const langKey = translation.multi_lang_key_master.lang_key;
|
||||
const langKey = translation.lang_key;
|
||||
if (params.langKeys.includes(langKey)) {
|
||||
result[langKey] = translation.lang_text;
|
||||
}
|
||||
|
|
@ -755,29 +830,31 @@ export class MultiLangService {
|
|||
logger.info("언어 삭제 시작", { langCode });
|
||||
|
||||
// 기존 언어 확인
|
||||
const existingLanguage = await prisma.language_master.findUnique({
|
||||
where: { lang_code: langCode },
|
||||
});
|
||||
const existingLanguage = await queryOne<{ lang_code: string }>(
|
||||
`SELECT lang_code FROM language_master WHERE lang_code = $1`,
|
||||
[langCode]
|
||||
);
|
||||
|
||||
if (!existingLanguage) {
|
||||
throw new Error(`언어를 찾을 수 없습니다: ${langCode}`);
|
||||
}
|
||||
|
||||
// 트랜잭션으로 언어와 관련 텍스트 삭제
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await transaction(async (client) => {
|
||||
// 해당 언어의 다국어 텍스트 삭제
|
||||
const deleteResult = await tx.multi_lang_text.deleteMany({
|
||||
where: { lang_code: langCode },
|
||||
});
|
||||
const deleteResult = await client.query(
|
||||
`DELETE FROM multi_lang_text WHERE lang_code = $1`,
|
||||
[langCode]
|
||||
);
|
||||
|
||||
logger.info(`삭제된 다국어 텍스트 수: ${deleteResult.count}`, {
|
||||
logger.info(`삭제된 다국어 텍스트 수: ${deleteResult.rowCount}`, {
|
||||
langCode,
|
||||
});
|
||||
|
||||
// 언어 마스터 삭제
|
||||
await tx.language_master.delete({
|
||||
where: { lang_code: langCode },
|
||||
});
|
||||
await client.query(`DELETE FROM language_master WHERE lang_code = $1`, [
|
||||
langCode,
|
||||
]);
|
||||
});
|
||||
|
||||
logger.info("언어 삭제 완료", { langCode });
|
||||
|
|
|
|||
Loading…
Reference in New Issue