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,