# ๐ŸŽจ 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 ์ฟผ๋ฆฌ ํฌํ•จ