From 45ec38790b3e8a31cc45688278b3aea5900f525e Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 1 Oct 2025 11:20:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203.6=20CollectionService=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EC=99=84=EB=A3=8C=20=EB=B0=8F=20Phase=203?= =?UTF-8?q?.7-3.9=20=EA=B3=84=ED=9A=8D=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CollectionService 전환 완료: - 11개 Prisma 호출을 모두 Raw Query로 전환 - 수집 설정 CRUD (getCollectionConfigs, getCollectionConfigById, createCollectionConfig, updateCollectionConfig, deleteCollectionConfig) - 수집 작업 관리 (executeCollection, getCollectionJobs, getCollectionHistory) - 동적 WHERE 조건 생성 (ILIKE 검색, OR 조건) - 동적 UPDATE 쿼리 (변경된 필드만 업데이트) - JSON 필드 처리 (collection_options) - LEFT JOIN (작업 목록 조회 시 설정 정보 포함) - 비동기 작업 처리 (setTimeout 내 query 사용) - 필드명 수정 (schedule_expression → schedule_cron) - TypeScript 컴파일 성공 - Prisma import 완전 제거 Phase 3 남은 서비스 계획서 작성: - PHASE3.7_LAYOUT_SERVICE_MIGRATION.md (10개 호출) - 레이아웃 표준 관리 (CRUD, 통계, JSON 필드) - PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md (10개 호출) - DB 타입 카테고리 관리 (CRUD, 통계, UPSERT) - PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md (6개 호출) - 템플릿 표준 관리 (복합 키, JSON 필드, DISTINCT) Phase 3 진행률: 87/162 (53.7%) 전체 진행률: 338/444 (76.1%) --- PHASE3.7_LAYOUT_SERVICE_MIGRATION.md | 369 +++++++++++++ ...E3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md | 484 ++++++++++++++++++ ...3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md | 391 ++++++++++++++ PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md | 22 +- .../src/services/collectionService.ts | 227 ++++---- 5 files changed, 1395 insertions(+), 98 deletions(-) create mode 100644 PHASE3.7_LAYOUT_SERVICE_MIGRATION.md create mode 100644 PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md create mode 100644 PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md diff --git a/PHASE3.7_LAYOUT_SERVICE_MIGRATION.md b/PHASE3.7_LAYOUT_SERVICE_MIGRATION.md new file mode 100644 index 00000000..74d1e0a9 --- /dev/null +++ b/PHASE3.7_LAYOUT_SERVICE_MIGRATION.md @@ -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( + `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( + `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 쿼리 포함 + diff --git a/PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md b/PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md new file mode 100644 index 00000000..aa691741 --- /dev/null +++ b/PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md @@ -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( + `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( + `SELECT * FROM db_type_categories WHERE type_code = $1`, + [data.type_code] +); + +if (existing) { + return { + success: false, + message: "이미 존재하는 타입 코드입니다." + }; +} + +const category = await queryOne( + `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( + `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` 타입을 반환하므로, 에러 처리를 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` 형식으로 응답을 반환합니다. Raw Query 전환 후에도 이 패턴을 유지해야 합니다. + +### 사전 정의 카테고리 +`syncPredefinedCategories()` 메서드는 시스템 초기화 시 사전 정의된 DB 타입 카테고리를 동기화합니다. UPSERT 로직이 필수입니다. + +### 외래 키 확인 +카테고리 삭제 시 `external_db_connections` 테이블에서 사용 중인지 확인하여 데이터 무결성을 보장합니다. + +--- + +**작성일**: 2025-10-01 +**예상 소요 시간**: 1시간 +**담당자**: 백엔드 개발팀 +**우선순위**: 🟡 중간 (Phase 3.8) +**상태**: ⏳ **대기 중** +**특이사항**: ApiResponse 래퍼, UPSERT, GROUP BY + LEFT JOIN 포함 + diff --git a/PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md b/PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md new file mode 100644 index 00000000..58713954 --- /dev/null +++ b/PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md @@ -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( + `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( + `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( + `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( + `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 쿼리 포함 + diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index 3a689bfb..945153dd 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -127,7 +127,7 @@ backend-node/ (루트) - `componentStandardService.ts` (0개) - ✅ **전환 완료** (Phase 3.3) - `commonCodeService.ts` (0개) - ✅ **전환 완료** (Phase 3.4) - `dataflowDiagramService.ts` (0개) - ✅ **전환 완료** (Phase 3.5) -- `collectionService.ts` (11개) - 컬렉션 관리 +- `collectionService.ts` (0개) - ✅ **전환 완료** (Phase 3.6) - `layoutService.ts` (10개) - 레이아웃 관리 - `dbTypeCategoryService.ts` (10개) - DB 타입 분류 ⭐ 신규 발견 - `templateStandardService.ts` (9개) - 템플릿 표준 @@ -1174,17 +1174,27 @@ describe("Performance Benchmarks", () => { - [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 완전 제거 - [ ] 배치 관련 서비스 전환 (26개) ⭐ 대규모 신규 발견 - [ ] BatchExternalDbService (8개) - [ ] BatchExecutionLogService (7개), BatchManagementService (5개) - [ ] BatchSchedulerService (4개) -- [ ] 표준 관리 서비스 전환 (10개) - - [ ] LayoutService (10개) +- [ ] 표준 관리 서비스 전환 (16개) + - [ ] LayoutService (10개) - [계획서](PHASE3.7_LAYOUT_SERVICE_MIGRATION.md) + - [ ] TemplateStandardService (6개) - [계획서](PHASE3.9_TEMPLATE_STANDARD_SERVICE_MIGRATION.md) - [ ] 데이터플로우 관련 서비스 (6개) ⭐ 신규 발견 - [ ] DataflowControlService (6개) -- [ ] 기타 중요 서비스 (38개) ⭐ 신규 발견 - - [ ] CollectionService (11개), DbTypeCategoryService (10개) - - [ ] TemplateStandardService (9개), DDLAuditLogger (8개) +- [ ] 기타 중요 서비스 (18개) ⭐ 신규 발견 + - [ ] DbTypeCategoryService (10개) - [계획서](PHASE3.8_DB_TYPE_CATEGORY_SERVICE_MIGRATION.md) + - [ ] DDLAuditLogger (8개) - [ ] 기능별 테스트 완료 ### **Phase 4: 확장 기능 (2.5주) - 129개 호출 ⭐ 대폭 확장** diff --git a/backend-node/src/services/collectionService.ts b/backend-node/src/services/collectionService.ts index 020e96f8..792f32ad 100644 --- a/backend-node/src/services/collectionService.ts +++ b/backend-node/src/services/collectionService.ts @@ -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 { - 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( + `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 { - const config = await prisma.data_collection_configs.findUnique({ - where: { id }, - }); + const config = await queryOne( + `SELECT * FROM data_collection_configs WHERE id = $1`, + [id] + ); if (!config) return null; @@ -84,15 +87,26 @@ export class CollectionService { data: DataCollectionConfig ): Promise { 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( + `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 ): Promise { - 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( + `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 { - 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 { - const config = await prisma.data_collection_configs.findUnique({ - where: { id: configId }, - }); + const config = await queryOne( + `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( + `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 { - 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(sql, values); return jobs as CollectionJob[]; } @@ -227,11 +268,13 @@ export class CollectionService { static async getCollectionHistory( configId: number ): Promise { - const history = await prisma.data_collection_jobs.findMany({ - where: { config_id: configId }, - orderBy: { started_at: "desc" }, - take: 50, // 최근 50개 이력 - }); + const history = await query( + `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,