# πŸ–₯️ Phase 2.1: ScreenManagementService Raw Query μ „ν™˜ κ³„νš ## πŸ“‹ κ°œμš” ScreenManagementServiceλŠ” **46개의 Prisma 호좜**이 μžˆλŠ” κ°€μž₯ λ³΅μž‘ν•œ μ„œλΉ„μŠ€μž…λ‹ˆλ‹€. ν™”λ©΄ μ •μ˜, λ ˆμ΄μ•„μ›ƒ, 메뉴 ν• λ‹Ή, ν…œν”Œλ¦Ώ λ“± λ‹€μ–‘ν•œ κΈ°λŠ₯을 ν¬ν•¨ν•©λ‹ˆλ‹€. ### πŸ“Š κΈ°λ³Έ 정보 | ν•­λͺ© | λ‚΄μš© | |------|------| | 파일 μœ„μΉ˜ | `backend-node/src/services/screenManagementService.ts` | | 파일 크기 | 1,700+ 라인 | | Prisma 호좜 | 46개 | | **ν˜„μž¬ μ§„ν–‰λ₯ ** | **17/46 (37.0%)** βœ… | | λ³΅μž‘λ„ | 맀우 λ†’μŒ | | μš°μ„ μˆœμœ„ | πŸ”΄ μ΅œμš°μ„  | ### 🎯 μ „ν™˜ ν˜„ν™© (2025-09-30 μ—…λ°μ΄νŠΈ) - βœ… **Stage 1 μ™„λ£Œ**: κΈ°λ³Έ CRUD (8개 ν•¨μˆ˜) - Commit: 13c1bc4, 0e8d1d4 - βœ… **Stage 2 μ™„λ£Œ**: λ ˆμ΄μ•„μ›ƒ 관리 (2개 ν•¨μˆ˜, 4 Prisma 호좜) - Commit: 67dced7 - βœ… **Stage 3 μ™„λ£Œ**: ν…œν”Œλ¦Ώ & 메뉴 관리 (5개 ν•¨μˆ˜) - Commit: 74351e8 - πŸ”„ **Stage 4 μ§„ν–‰ 쀑**: λ³΅μž‘ν•œ κΈ°λŠ₯ (νŠΈλžœμž­μ…˜) --- ## πŸ” Prisma μ‚¬μš© ν˜„ν™© 뢄석 ### 1. ν™”λ©΄ μ •μ˜ 관리 (Screen Definitions) - 18개 ```typescript // Line 53: ν™”λ©΄ μ½”λ“œ 쀑볡 확인 await prisma.screen_definitions.findFirst({ where: { screen_code, is_active: { not: "D" } } }) // Line 70: ν™”λ©΄ 생성 await prisma.screen_definitions.create({ data: { ... } }) // Line 99: ν™”λ©΄ λͺ©λ‘ 쑰회 (νŽ˜μ΄μ§•) await prisma.screen_definitions.findMany({ where, skip, take, orderBy }) // Line 105: ν™”λ©΄ 총 개수 await prisma.screen_definitions.count({ where }) // Line 166: 전체 ν™”λ©΄ λͺ©λ‘ await prisma.screen_definitions.findMany({ where }) // Line 178: ν™”λ©΄ μ½”λ“œλ‘œ 쑰회 await prisma.screen_definitions.findFirst({ where: { screen_code } }) // Line 205: ν™”λ©΄ ID둜 쑰회 await prisma.screen_definitions.findFirst({ where: { screen_id } }) // Line 221: ν™”λ©΄ 쑴재 확인 await prisma.screen_definitions.findUnique({ where: { screen_id } }) // Line 236: ν™”λ©΄ μ—…λ°μ΄νŠΈ await prisma.screen_definitions.update({ where, data }) // Line 268: ν™”λ©΄ 볡사 - 원본 쑰회 await prisma.screen_definitions.findUnique({ where, include: { screen_layouts } }) // Line 292: ν™”λ©΄ μˆœμ„œ λ³€κ²½ - 전체 쑰회 await prisma.screen_definitions.findMany({ where }) // Line 486: ν™”λ©΄ ν…œν”Œλ¦Ώ 적용 - 쑴재 확인 await prisma.screen_definitions.findUnique({ where }) // Line 557: ν™”λ©΄ 볡사 - 쑴재 확인 await prisma.screen_definitions.findUnique({ where }) // Line 578: ν™”λ©΄ 볡사 - 쀑볡 확인 await prisma.screen_definitions.findFirst({ where }) // Line 651: ν™”λ©΄ μ‚­μ œ - 쑴재 확인 await prisma.screen_definitions.findUnique({ where }) // Line 672: ν™”λ©΄ μ‚­μ œ (물리 μ‚­μ œ) await prisma.screen_definitions.delete({ where }) // Line 700: μ‚­μ œλœ ν™”λ©΄ 쑰회 await prisma.screen_definitions.findMany({ where: { is_active: "D" } }) // Line 706: μ‚­μ œλœ ν™”λ©΄ 개수 await prisma.screen_definitions.count({ where }) // Line 763: 일괄 μ‚­μ œ - ν™”λ©΄ 쑰회 await prisma.screen_definitions.findMany({ where }) // Line 1083: λ ˆμ΄μ•„μ›ƒ μ €μž₯ - ν™”λ©΄ 확인 await prisma.screen_definitions.findUnique({ where }) // Line 1181: λ ˆμ΄μ•„μ›ƒ 쑰회 - ν™”λ©΄ 확인 await prisma.screen_definitions.findUnique({ where }) // Line 1655: μœ„μ ― 데이터 μ €μž₯ - ν™”λ©΄ 쑴재 확인 await prisma.screen_definitions.findMany({ where }) ``` ### 2. λ ˆμ΄μ•„μ›ƒ 관리 (Screen Layouts) - 4개 ```typescript // Line 1096: λ ˆμ΄μ•„μ›ƒ μ‚­μ œ await prisma.screen_layouts.deleteMany({ where: { screen_id } }) // Line 1107: λ ˆμ΄μ•„μ›ƒ 생성 (단일) await prisma.screen_layouts.create({ data }) // Line 1152: λ ˆμ΄μ•„μ›ƒ 생성 (닀쀑) await prisma.screen_layouts.create({ data }) // Line 1193: λ ˆμ΄μ•„μ›ƒ 쑰회 await prisma.screen_layouts.findMany({ where }) ``` ### 3. ν…œν”Œλ¦Ώ 관리 (Screen Templates) - 2개 ```typescript // Line 1303: ν…œν”Œλ¦Ώ λͺ©λ‘ 쑰회 await prisma.screen_templates.findMany({ where }) // Line 1317: ν…œν”Œλ¦Ώ 생성 await prisma.screen_templates.create({ data }) ``` ### 4. 메뉴 ν• λ‹Ή (Screen Menu Assignments) - 5개 ```typescript // Line 446: 메뉴 ν• λ‹Ή 쑰회 await prisma.screen_menu_assignments.findMany({ where }) // Line 1346: 메뉴 ν• λ‹Ή 쀑볡 확인 await prisma.screen_menu_assignments.findFirst({ where }) // Line 1358: 메뉴 ν• λ‹Ή 생성 await prisma.screen_menu_assignments.create({ data }) // Line 1376: 화면별 메뉴 ν• λ‹Ή 쑰회 await prisma.screen_menu_assignments.findMany({ where }) // Line 1401: 메뉴 ν• λ‹Ή μ‚­μ œ await prisma.screen_menu_assignments.deleteMany({ where }) ``` ### 5. ν…Œμ΄λΈ” λ ˆμ΄λΈ” (Table Labels) - 3개 ```typescript // Line 117: ν…Œμ΄λΈ” λ ˆμ΄λΈ” 쑰회 (νŽ˜μ΄μ§•) await prisma.table_labels.findMany({ where, skip, take }) // Line 713: ν…Œμ΄λΈ” λ ˆμ΄λΈ” 쑰회 (전체) await prisma.table_labels.findMany({ where }) ``` ### 6. 컬럼 λ ˆμ΄λΈ” (Column Labels) - 2개 ```typescript // Line 948: μ›Ήνƒ€μž… 정보 쑰회 await prisma.column_labels.findMany({ where, select }) // Line 1456: 컬럼 λ ˆμ΄λΈ” UPSERT await prisma.column_labels.upsert({ where, create, update }) ``` ### 7. Raw Query μ‚¬μš© (이미 있음) - 6개 ```typescript // Line 627: ν™”λ©΄ μˆœμ„œ λ³€κ²½ (일괄 μ—…λ°μ΄νŠΈ) await prisma.$executeRaw`UPDATE screen_definitions SET display_order = ...` // Line 833: ν…Œμ΄λΈ” λͺ©λ‘ 쑰회 await prisma.$queryRaw>`SELECT tablename ...` // Line 876: ν…Œμ΄λΈ” 쑴재 확인 await prisma.$queryRaw>`SELECT tablename ...` // Line 922: ν…Œμ΄λΈ” 컬럼 정보 쑰회 await prisma.$queryRaw>`SELECT column_name, data_type ...` // Line 1418: 컬럼 정보 쑰회 (상세) await prisma.$queryRaw`SELECT column_name, data_type ...` ``` ### 8. νŠΈλžœμž­μ…˜ μ‚¬μš© - 3개 ```typescript // Line 521: ν™”λ©΄ ν…œν”Œλ¦Ώ 적용 νŠΈλžœμž­μ…˜ await prisma.$transaction(async (tx) => { ... }) // Line 593: ν™”λ©΄ 볡사 νŠΈλžœμž­μ…˜ await prisma.$transaction(async (tx) => { ... }) // Line 788: 일괄 μ‚­μ œ νŠΈλžœμž­μ…˜ await prisma.$transaction(async (tx) => { ... }) // Line 1697: μœ„μ ― 데이터 μ €μž₯ νŠΈλžœμž­μ…˜ await prisma.$transaction(async (tx) => { ... }) ``` --- ## πŸ› οΈ μ „ν™˜ μ „λž΅ ### μ „λž΅ 1: 단계적 μ „ν™˜ 1. **1단계**: λ‹¨μˆœ CRUD μ „ν™˜ (findFirst, findMany, create, update, delete) 2. **2단계**: λ³΅μž‘ν•œ 쑰회 μ „ν™˜ (include, join) 3. **3단계**: νŠΈλžœμž­μ…˜ μ „ν™˜ 4. **4단계**: Raw Query κ°œμ„  ### μ „λž΅ 2: ν•¨μˆ˜λ³„ μ „ν™˜ μš°μ„ μˆœμœ„ #### πŸ”΄ μ΅œμš°μ„  (κΈ°λ³Έ CRUD) - `createScreen()` - Line 70 - `getScreensByCompany()` - Line 99-105 - `getScreenByCode()` - Line 178 - `getScreenById()` - Line 205 - `updateScreen()` - Line 236 - `deleteScreen()` - Line 672 #### 🟑 2μˆœμœ„ (λ ˆμ΄μ•„μ›ƒ) - `saveLayout()` - Line 1096-1152 - `getLayout()` - Line 1193 - `deleteLayout()` - Line 1096 #### 🟒 3μˆœμœ„ (ν…œν”Œλ¦Ώ & 메뉴) - `getTemplates()` - Line 1303 - `createTemplate()` - Line 1317 - `assignToMenu()` - Line 1358 - `getMenuAssignments()` - Line 1376 - `removeMenuAssignment()` - Line 1401 #### πŸ”΅ 4μˆœμœ„ (λ³΅μž‘ν•œ κΈ°λŠ₯) - `copyScreen()` - Line 593 (νŠΈλžœμž­μ…˜) - `applyTemplate()` - Line 521 (νŠΈλžœμž­μ…˜) - `bulkDelete()` - Line 788 (νŠΈλžœμž­μ…˜) - `reorderScreens()` - Line 627 (Raw Query) --- ## πŸ“ μ „ν™˜ μ˜ˆμ‹œ ### μ˜ˆμ‹œ 1: createScreen() μ „ν™˜ **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript // Line 53: 쀑볡 확인 const existingScreen = await prisma.screen_definitions.findFirst({ where: { screen_code: screenData.screenCode, is_active: { not: "D" }, }, }); // Line 70: 생성 const screen = await prisma.screen_definitions.create({ data: { screen_name: screenData.screenName, screen_code: screenData.screenCode, table_name: screenData.tableName, company_code: screenData.companyCode, description: screenData.description, created_by: screenData.createdBy, }, }); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript import { query } from "../database/db"; // 쀑볡 확인 const existingResult = await query<{ screen_id: number }>( `SELECT screen_id FROM screen_definitions WHERE screen_code = $1 AND is_active != 'D' LIMIT 1`, [screenData.screenCode] ); if (existingResult.length > 0) { throw new Error("이미 μ‘΄μž¬ν•˜λŠ” ν™”λ©΄ μ½”λ“œμž…λ‹ˆλ‹€."); } // 생성 const [screen] = await query( `INSERT INTO screen_definitions ( screen_name, screen_code, table_name, company_code, description, created_by ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [ screenData.screenName, screenData.screenCode, screenData.tableName, screenData.companyCode, screenData.description, screenData.createdBy, ] ); ``` ### μ˜ˆμ‹œ 2: getScreensByCompany() μ „ν™˜ (νŽ˜μ΄μ§•) **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript const [screens, total] = await Promise.all([ prisma.screen_definitions.findMany({ where: whereClause, skip: (page - 1) * size, take: size, orderBy: { created_at: "desc" }, }), prisma.screen_definitions.count({ where: whereClause }), ]); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript const offset = (page - 1) * size; const whereSQL = companyCode !== "*" ? "WHERE company_code = $1 AND is_active != 'D'" : "WHERE is_active != 'D'"; const params = companyCode !== "*" ? [companyCode, size, offset] : [size, offset]; const [screens, totalResult] = await Promise.all([ query( `SELECT * FROM screen_definitions ${whereSQL} ORDER BY created_at DESC LIMIT $${params.length - 1} OFFSET $${params.length}`, params ), query<{ count: number }>( `SELECT COUNT(*) as count FROM screen_definitions ${whereSQL}`, companyCode !== "*" ? [companyCode] : [] ), ]); const total = totalResult[0]?.count || 0; ``` ### μ˜ˆμ‹œ 3: νŠΈλžœμž­μ…˜ μ „ν™˜ **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript await prisma.$transaction(async (tx) => { const newScreen = await tx.screen_definitions.create({ data: { ... } }); await tx.screen_layouts.createMany({ data: layouts }); }); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript import { transaction } from "../database/db"; await transaction(async (client) => { const [newScreen] = await client.query( `INSERT INTO screen_definitions (...) VALUES (...) RETURNING *`, [...] ); for (const layout of layouts) { await client.query( `INSERT INTO screen_layouts (...) VALUES (...)`, [...] ); } }); ``` --- ## πŸ§ͺ ν…ŒμŠ€νŠΈ κ³„νš ### λ‹¨μœ„ ν…ŒμŠ€νŠΈ ```typescript describe("ScreenManagementService Raw Query μ „ν™˜ ν…ŒμŠ€νŠΈ", () => { describe("createScreen", () => { test("ν™”λ©΄ 생성 성곡", async () => { ... }); test("쀑볡 ν™”λ©΄ μ½”λ“œ μ—λŸ¬", async () => { ... }); }); describe("getScreensByCompany", () => { test("νŽ˜μ΄μ§• 쑰회 성곡", async () => { ... }); test("νšŒμ‚¬λ³„ 필터링", async () => { ... }); }); describe("copyScreen", () => { test("ν™”λ©΄ 볡사 성곡 (νŠΈλžœμž­μ…˜)", async () => { ... }); test("λ ˆμ΄μ•„μ›ƒ ν•¨κ»˜ 볡사", async () => { ... }); }); }); ``` ### 톡합 ν…ŒμŠ€νŠΈ ```typescript describe("ν™”λ©΄ 관리 톡합 ν…ŒμŠ€νŠΈ", () => { test("ν™”λ©΄ 생성 β†’ 쑰회 β†’ μˆ˜μ • β†’ μ‚­μ œ", async () => { ... }); test("ν™”λ©΄ 볡사 β†’ λ ˆμ΄μ•„μ›ƒ 확인", async () => { ... }); test("메뉴 ν• λ‹Ή β†’ 쑰회 β†’ ν•΄μ œ", async () => { ... }); }); ``` --- ## πŸ“‹ 체크리슀트 ### 1단계: κΈ°λ³Έ CRUD (8개 ν•¨μˆ˜) βœ… **μ™„λ£Œ** - [x] `createScreen()` - ν™”λ©΄ 생성 - [x] `getScreensByCompany()` - ν™”λ©΄ λͺ©λ‘ (νŽ˜μ΄μ§•) - [x] `getScreenByCode()` - ν™”λ©΄ μ½”λ“œλ‘œ 쑰회 - [x] `getScreenById()` - ν™”λ©΄ ID둜 쑰회 - [x] `updateScreen()` - ν™”λ©΄ μ—…λ°μ΄νŠΈ - [x] `deleteScreen()` - ν™”λ©΄ μ‚­μ œ - [x] `getScreens()` - 전체 ν™”λ©΄ λͺ©λ‘ 쑰회 - [x] `getScreen()` - νšŒμ‚¬ μ½”λ“œ 필터링 포함 쑰회 ### 2단계: λ ˆμ΄μ•„μ›ƒ 관리 (2개 ν•¨μˆ˜) βœ… **μ™„λ£Œ** - [x] `saveLayout()` - λ ˆμ΄μ•„μ›ƒ μ €μž₯ (메타데이터 + μ»΄ν¬λ„ŒνŠΈ) - [x] `getLayout()` - λ ˆμ΄μ•„μ›ƒ 쑰회 - [x] λ ˆμ΄μ•„μ›ƒ μ‚­μ œ 둜직 (saveLayout 내뢀에 포함) ### 3단계: ν…œν”Œλ¦Ώ & 메뉴 (5개 ν•¨μˆ˜) βœ… **μ™„λ£Œ** - [x] `getTemplatesByCompany()` - ν…œν”Œλ¦Ώ λͺ©λ‘ - [x] `createTemplate()` - ν…œν”Œλ¦Ώ 생성 - [x] `assignScreenToMenu()` - 메뉴 ν• λ‹Ή - [x] `getScreensByMenu()` - 메뉴별 ν™”λ©΄ 쑰회 - [x] `unassignScreenFromMenu()` - 메뉴 ν• λ‹Ή ν•΄μ œ - [ ] ν…Œμ΄λΈ” λ ˆμ΄λΈ” 쑰회 (getScreensByCompany 내뢀에 포함됨) ### 4단계: λ³΅μž‘ν•œ κΈ°λŠ₯ (4개 ν•¨μˆ˜) πŸ”„ **μ§„ν–‰ 쀑** - [ ] `copyScreen()` - ν™”λ©΄ 볡사 (νŠΈλžœμž­μ…˜) - [ ] `applyTemplate()` - ν…œν”Œλ¦Ώ 적용 (νŠΈλžœμž­μ…˜) - [ ] `bulkDelete()` - 일괄 μ‚­μ œ (νŠΈλžœμž­μ…˜) - [ ] `reorderScreens()` - μˆœμ„œ λ³€κ²½ (Raw Query) ### 5단계: ν…ŒμŠ€νŠΈ & 검증 - [ ] λ‹¨μœ„ ν…ŒμŠ€νŠΈ μž‘μ„± (20개 이상) - [ ] 톡합 ν…ŒμŠ€νŠΈ μž‘μ„± (5개 이상) - [ ] μ„±λŠ₯ ν…ŒμŠ€νŠΈ - [ ] Prisma import 제거 확인 --- ## 🎯 μ™„λ£Œ κΈ°μ€€ - βœ… 46개 Prisma 호좜 λͺ¨λ‘ Raw Query둜 μ „ν™˜ - βœ… λͺ¨λ“  λ‹¨μœ„ ν…ŒμŠ€νŠΈ 톡과 - βœ… λͺ¨λ“  톡합 ν…ŒμŠ€νŠΈ 톡과 - βœ… μ„±λŠ₯ μ €ν•˜ μ—†μŒ (κΈ°μ‘΄ λŒ€λΉ„ Β±10% 이내) - βœ… νŠΈλžœμž­μ…˜ 정상 λ™μž‘ 확인 - βœ… μ—λŸ¬ 처리 및 λ‘€λ°± 정상 λ™μž‘ --- **μž‘μ„±μΌ**: 2025-09-30 **μ˜ˆμƒ μ†Œμš” μ‹œκ°„**: 2-3일 **λ‹΄λ‹Ήμž**: λ°±μ—”λ“œ κ°œλ°œνŒ€ **μš°μ„ μˆœμœ„**: πŸ”΄ μ΅œμš°μ„  (Phase 2.1)