# πŸ–₯️ Phase 2.1: ScreenManagementService Raw Query μ „ν™˜ κ³„νš ## πŸ“‹ κ°œμš” ScreenManagementServiceλŠ” **46개의 Prisma 호좜**이 μžˆλŠ” κ°€μž₯ λ³΅μž‘ν•œ μ„œλΉ„μŠ€μž…λ‹ˆλ‹€. ν™”λ©΄ μ •μ˜, λ ˆμ΄μ•„μ›ƒ, 메뉴 ν• λ‹Ή, ν…œν”Œλ¦Ώ λ“± λ‹€μ–‘ν•œ κΈ°λŠ₯을 ν¬ν•¨ν•©λ‹ˆλ‹€. ### πŸ“Š κΈ°λ³Έ 정보 | ν•­λͺ© | λ‚΄μš© | | --------------- | ------------------------------------------------------ | | 파일 μœ„μΉ˜ | `backend-node/src/services/screenManagementService.ts` | | 파일 크기 | 1,700+ 라인 | | Prisma 호좜 | 46개 | | **ν˜„μž¬ μ§„ν–‰λ₯ ** | **46/46 (100%)** βœ… **μ™„λ£Œ** | | λ³΅μž‘λ„ | 맀우 λ†’μŒ | | μš°μ„ μˆœμœ„ | πŸ”΄ μ΅œμš°μ„  | ### 🎯 μ „ν™˜ ν˜„ν™© (2025-09-30 μ—…λ°μ΄νŠΈ) - βœ… **Stage 1 μ™„λ£Œ**: κΈ°λ³Έ CRUD (8개 ν•¨μˆ˜) - Commit: 13c1bc4, 0e8d1d4 - βœ… **Stage 2 μ™„λ£Œ**: λ ˆμ΄μ•„μ›ƒ 관리 (2개 ν•¨μˆ˜, 4 Prisma 호좜) - Commit: 67dced7 - βœ… **Stage 3 μ™„λ£Œ**: ν…œν”Œλ¦Ώ & 메뉴 관리 (5개 ν•¨μˆ˜) - Commit: 74351e8 - βœ… **Stage 4 μ™„λ£Œ**: λ³΅μž‘ν•œ κΈ°λŠ₯ (νŠΈλžœμž­μ…˜) - **λͺ¨λ“  46개 Prisma 호좜 μ „ν™˜ μ™„λ£Œ** --- ## πŸ” 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개 ν•¨μˆ˜) βœ… **μ™„λ£Œ** - [x] `copyScreen()` - ν™”λ©΄ 볡사 (νŠΈλžœμž­μ…˜) - [x] `generateScreenCode()` - ν™”λ©΄ μ½”λ“œ μžλ™ 생성 - [x] `checkScreenDependencies()` - ν™”λ©΄ μ˜μ‘΄μ„± 체크 (메뉴 ν• λ‹Ή 포함) - [x] λͺ¨λ“  μœ ν‹Έλ¦¬ν‹° λ©”μ„œλ“œ Raw Query μ „ν™˜ ### 5단계: ν…ŒμŠ€νŠΈ & 검증 βœ… **μ™„λ£Œ** - [x] λ‹¨μœ„ ν…ŒμŠ€νŠΈ μž‘μ„± (18개 ν…ŒμŠ€νŠΈ 톡과) - createScreen, updateScreen, deleteScreen - getScreensByCompany, getScreenById - saveLayout, getLayout - getTemplatesByCompany, assignScreenToMenu - copyScreen, generateScreenCode - getTableColumns - [x] 톡합 ν…ŒμŠ€νŠΈ μž‘μ„± (6개 μ‹œλ‚˜λ¦¬μ˜€) - ν™”λ©΄ 생λͺ…μ£ΌκΈ° ν…ŒμŠ€νŠΈ (생성 β†’ 쑰회 β†’ μˆ˜μ • β†’ μ‚­μ œ β†’ 볡원 β†’ μ˜κ΅¬μ‚­μ œ) - ν™”λ©΄ 볡사 및 λ ˆμ΄μ•„μ›ƒ ν…ŒμŠ€νŠΈ - ν…Œμ΄λΈ” 정보 쑰회 ν…ŒμŠ€νŠΈ - 일괄 μž‘μ—… ν…ŒμŠ€νŠΈ - ν™”λ©΄ μ½”λ“œ μžλ™ 생성 ν…ŒμŠ€νŠΈ - [x] Prisma import μ™„μ „ 제거 확인 - [ ] μ„±λŠ₯ ν…ŒμŠ€νŠΈ (μΆ”ν›„ μ‹€ν–‰ μ˜ˆμ •) --- ## 🎯 μ™„λ£Œ κΈ°μ€€ - βœ… **46개 Prisma 호좜 λͺ¨λ‘ Raw Query둜 μ „ν™˜ μ™„λ£Œ** - βœ… **λͺ¨λ“  TypeScript 컴파일 였λ₯˜ ν•΄κ²°** - βœ… **νŠΈλžœμž­μ…˜ 정상 λ™μž‘ 확인** - βœ… **μ—λŸ¬ 처리 및 λ‘€λ°± 정상 λ™μž‘** - βœ… **λͺ¨λ“  λ‹¨μœ„ ν…ŒμŠ€νŠΈ 톡과 (18개)** - βœ… **λͺ¨λ“  톡합 ν…ŒμŠ€νŠΈ μž‘μ„± μ™„λ£Œ (6개 μ‹œλ‚˜λ¦¬μ˜€)** - βœ… **Prisma import μ™„μ „ 제거** - [ ] μ„±λŠ₯ μ €ν•˜ μ—†μŒ (κΈ°μ‘΄ λŒ€λΉ„ Β±10% 이내) - μΆ”ν›„ μΈ‘μ • μ˜ˆμ • ## πŸ“Š ν…ŒμŠ€νŠΈ κ²°κ³Ό ### λ‹¨μœ„ ν…ŒμŠ€νŠΈ (18개) ``` βœ… createScreen - ν™”λ©΄ 생성 (2개 ν…ŒμŠ€νŠΈ) βœ… getScreensByCompany - ν™”λ©΄ λͺ©λ‘ νŽ˜μ΄μ§• (2개 ν…ŒμŠ€νŠΈ) βœ… updateScreen - ν™”λ©΄ μ—…λ°μ΄νŠΈ (2개 ν…ŒμŠ€νŠΈ) βœ… deleteScreen - ν™”λ©΄ μ‚­μ œ (2개 ν…ŒμŠ€νŠΈ) βœ… saveLayout - λ ˆμ΄μ•„μ›ƒ μ €μž₯ (2개 ν…ŒμŠ€νŠΈ) - κΈ°λ³Έ μ €μž₯, μ†Œμˆ˜μ  μ’Œν‘œ 반올림 처리 βœ… getLayout - λ ˆμ΄μ•„μ›ƒ 쑰회 (1개 ν…ŒμŠ€νŠΈ) βœ… getTemplatesByCompany - ν…œν”Œλ¦Ώ λͺ©λ‘ (1개 ν…ŒμŠ€νŠΈ) βœ… assignScreenToMenu - 메뉴 ν• λ‹Ή (2개 ν…ŒμŠ€νŠΈ) βœ… copyScreen - ν™”λ©΄ 볡사 (1개 ν…ŒμŠ€νŠΈ) βœ… generateScreenCode - ν™”λ©΄ μ½”λ“œ μžλ™ 생성 (2개 ν…ŒμŠ€νŠΈ) βœ… getTableColumns - ν…Œμ΄λΈ” 컬럼 정보 (1개 ν…ŒμŠ€νŠΈ) Test Suites: 1 passed Tests: 18 passed Time: 1.922s ``` ### 톡합 ν…ŒμŠ€νŠΈ (6개 μ‹œλ‚˜λ¦¬μ˜€) ``` βœ… ν™”λ©΄ 생λͺ…μ£ΌκΈ° ν…ŒμŠ€νŠΈ - 생성 β†’ 쑰회 β†’ μˆ˜μ • β†’ μ‚­μ œ β†’ 볡원 β†’ μ˜κ΅¬μ‚­μ œ βœ… ν™”λ©΄ 볡사 및 λ ˆμ΄μ•„μ›ƒ ν…ŒμŠ€νŠΈ - ν™”λ©΄ 볡사 β†’ λ ˆμ΄μ•„μ›ƒ μ €μž₯ β†’ λ ˆμ΄μ•„μ›ƒ 확인 β†’ λ ˆμ΄μ•„μ›ƒ μˆ˜μ • βœ… ν…Œμ΄λΈ” 정보 쑰회 ν…ŒμŠ€νŠΈ - ν…Œμ΄λΈ” λͺ©λ‘ 쑰회 β†’ νŠΉμ • ν…Œμ΄λΈ” 정보 쑰회 βœ… 일괄 μž‘μ—… ν…ŒμŠ€νŠΈ - μ—¬λŸ¬ ν™”λ©΄ 생성 β†’ 일괄 μ‚­μ œ βœ… ν™”λ©΄ μ½”λ“œ μžλ™ 생성 ν…ŒμŠ€νŠΈ - 순차적 ν™”λ©΄ μ½”λ“œ 생성 검증 βœ… 메뉴 ν• λ‹Ή ν…ŒμŠ€νŠΈ (skip - μ‹€μ œ 메뉴 데이터 ν•„μš”) ``` --- ## πŸ› 버그 μˆ˜μ • 및 κ°œμ„ μ‚¬ν•­ ### μ‹€μ œ 운영 ν™˜κ²½μ—μ„œ 발견된 이슈 #### 1. μ†Œμˆ˜μ  μ’Œν‘œ μ €μž₯ 였λ₯˜ (ν•΄κ²° μ™„λ£Œ) **문제**: ``` invalid input syntax for type integer: "1602.666666666667" ``` - `position_x`, `position_y`, `width`, `height` 컬럼이 `integer` νƒ€μž… - 격자 계산 μ‹œ μ†Œμˆ˜μ  값이 λ°œμƒν•˜μ—¬ μ €μž₯ μ‹€νŒ¨ **ν•΄κ²°**: ```typescript Math.round(component.position.x), // μ •μˆ˜λ‘œ 반올림 Math.round(component.position.y), Math.round(component.size.width), Math.round(component.size.height), ``` **ν…ŒμŠ€νŠΈ μΆ”κ°€**: - μ†Œμˆ˜μ  μ’Œν‘œ μ €μž₯ ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ μΆ”κ°€ - 반올림 처리 검증 **영ν–₯ λ²”μœ„**: - `saveLayout()` ν•¨μˆ˜ - `copyScreen()` ν•¨μˆ˜ (λ ˆμ΄μ•„μ›ƒ 볡사 μ‹œ) --- **μž‘μ„±μΌ**: 2025-09-30 **μ™„λ£ŒμΌ**: 2025-09-30 **μ˜ˆμƒ μ†Œμš” μ‹œκ°„**: 2-3일 β†’ **μ‹€μ œ μ†Œμš” μ‹œκ°„**: 1일 **λ‹΄λ‹Ήμž**: λ°±μ—”λ“œ κ°œλ°œνŒ€ **μš°μ„ μˆœμœ„**: πŸ”΄ μ΅œμš°μ„  (Phase 2.1) **μƒνƒœ**: βœ… **μ™„λ£Œ**