diff --git a/PHASE2_SCREEN_MANAGEMENT_MIGRATION.md b/PHASE2_SCREEN_MANAGEMENT_MIGRATION.md new file mode 100644 index 00000000..c54f2966 --- /dev/null +++ b/PHASE2_SCREEN_MANAGEMENT_MIGRATION.md @@ -0,0 +1,449 @@ +# πŸ–₯️ Phase 2.1: ScreenManagementService Raw Query μ „ν™˜ κ³„νš + +## πŸ“‹ κ°œμš” + +ScreenManagementServiceλŠ” **46개의 Prisma 호좜**이 μžˆλŠ” κ°€μž₯ λ³΅μž‘ν•œ μ„œλΉ„μŠ€μž…λ‹ˆλ‹€. ν™”λ©΄ μ •μ˜, λ ˆμ΄μ•„μ›ƒ, 메뉴 ν• λ‹Ή, ν…œν”Œλ¦Ώ λ“± λ‹€μ–‘ν•œ κΈ°λŠ₯을 ν¬ν•¨ν•©λ‹ˆλ‹€. + +### πŸ“Š κΈ°λ³Έ 정보 + +| ν•­λͺ© | λ‚΄μš© | +|------|------| +| 파일 μœ„μΉ˜ | `backend-node/src/services/screenManagementService.ts` | +| 파일 크기 | 1,700+ 라인 | +| Prisma 호좜 | 46개 | +| λ³΅μž‘λ„ | 맀우 λ†’μŒ | +| μš°μ„ μˆœμœ„ | πŸ”΄ μ΅œμš°μ„  | + +--- + +## πŸ” 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 (6개 ν•¨μˆ˜) +- [ ] `createScreen()` - ν™”λ©΄ 생성 +- [ ] `getScreensByCompany()` - ν™”λ©΄ λͺ©λ‘ (νŽ˜μ΄μ§•) +- [ ] `getScreenByCode()` - ν™”λ©΄ μ½”λ“œλ‘œ 쑰회 +- [ ] `getScreenById()` - ν™”λ©΄ ID둜 쑰회 +- [ ] `updateScreen()` - ν™”λ©΄ μ—…λ°μ΄νŠΈ +- [ ] `deleteScreen()` - ν™”λ©΄ μ‚­μ œ + +### 2단계: λ ˆμ΄μ•„μ›ƒ 관리 (3개 ν•¨μˆ˜) +- [ ] `saveLayout()` - λ ˆμ΄μ•„μ›ƒ μ €μž₯ +- [ ] `getLayout()` - λ ˆμ΄μ•„μ›ƒ 쑰회 +- [ ] λ ˆμ΄μ•„μ›ƒ μ‚­μ œ 둜직 + +### 3단계: ν…œν”Œλ¦Ώ & 메뉴 (6개 ν•¨μˆ˜) +- [ ] `getTemplates()` - ν…œν”Œλ¦Ώ λͺ©λ‘ +- [ ] `createTemplate()` - ν…œν”Œλ¦Ώ 생성 +- [ ] `assignToMenu()` - 메뉴 ν• λ‹Ή +- [ ] `getMenuAssignments()` - 메뉴 ν• λ‹Ή 쑰회 +- [ ] `removeMenuAssignment()` - 메뉴 ν• λ‹Ή ν•΄μ œ +- [ ] ν…Œμ΄λΈ” λ ˆμ΄λΈ” 쑰회 + +### 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) \ No newline at end of file