# πŸ—‚οΈ Phase 2.2: TableManagementService Raw Query μ „ν™˜ κ³„νš ## πŸ“‹ κ°œμš” TableManagementServiceλŠ” **33개의 Prisma 호좜**이 μžˆμŠ΅λ‹ˆλ‹€. λŒ€λΆ€λΆ„(μ•½ 26개)은 `$queryRaw`λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ–΄ SQL은 이미 μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, **Prisma ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ™„μ „νžˆ μ œκ±°ν•˜λ €λ©΄ 33개 λͺ¨λ‘λ₯Ό `db.ts`의 `query` ν•¨μˆ˜λ‘œ ꡐ체**ν•΄μ•Ό ν•©λ‹ˆλ‹€. ### πŸ“Š κΈ°λ³Έ 정보 | ν•­λͺ© | λ‚΄μš© | | --------------- | ----------------------------------------------------- | | 파일 μœ„μΉ˜ | `backend-node/src/services/tableManagementService.ts` | | 파일 크기 | 3,178 라인 | | Prisma 호좜 | 33개 ($queryRaw: 26개, ORM: 7개) | | **ν˜„μž¬ μ§„ν–‰λ₯ ** | **0/33 (0%)** ⏳ **μ „ν™˜ ν•„μš”** | | **μ „ν™˜ ν•„μš”** | **33개 λͺ¨λ‘ μ „ν™˜ ν•„μš”** (SQL은 이미 μž‘μ„±λ˜μ–΄ 있음) | | λ³΅μž‘λ„ | 쀑간 (SQL μž‘μ„±μ€ μ™„λ£Œ, `query()` ν•¨μˆ˜λ‘œ ꡐ체만 ν•„μš”) | | μš°μ„ μˆœμœ„ | 🟑 쀑간 (Phase 2.2) | ### 🎯 μ „ν™˜ λͺ©ν‘œ - βœ… **33개 λͺ¨λ“  Prisma ν˜ΈμΆœμ„ `db.ts`의 `query()` ν•¨μˆ˜λ‘œ ꡐ체** - 26개 `$queryRaw` β†’ `query()` λ˜λŠ” `queryOne()` - 7개 ORM λ©”μ„œλ“œ β†’ `query()` (SQL μƒˆλ‘œ μž‘μ„±) - 1개 `$transaction` β†’ `transaction()` - βœ… νŠΈλžœμž­μ…˜ 처리 정상 λ™μž‘ 확인 - βœ… λͺ¨λ“  λ‹¨μœ„ ν…ŒμŠ€νŠΈ 톡과 - βœ… **Prisma import μ™„μ „ 제거** --- ## πŸ” Prisma μ‚¬μš© ν˜„ν™© 뢄석 ### 1. `$queryRaw` / `$queryRawUnsafe` μ‚¬μš© (26개) **ν˜„μž¬ μƒνƒœ**: SQL은 이미 μž‘μ„±λ˜μ–΄ 있음 βœ… **μ „ν™˜ μž‘μ—…**: `prisma.$queryRaw` β†’ `query()` ν•¨μˆ˜λ‘œ ꡐ체만 ν•˜λ©΄ 됨 ```typescript // κΈ°μ‘΄ await prisma.$queryRaw`SELECT ...`; await prisma.$queryRawUnsafe(sqlString, ...params); // μ „ν™˜ ν›„ import { query } from "../database/db"; await query(`SELECT ...`); await query(sqlString, params); ``` ### 2. ORM λ©”μ„œλ“œ μ‚¬μš© (7개) **ν˜„μž¬ μƒνƒœ**: Prisma ORM λ©”μ„œλ“œ μ‚¬μš© **μ „ν™˜ μž‘μ—…**: SQL μž‘μ„± ν•„μš” #### 1. table_labels 관리 (2개) ```typescript // Line 254: ν…Œμ΄λΈ” 라벨 UPSERT await prisma.table_labels.upsert({ where: { table_name: tableName }, update: {}, create: { table_name, table_label, description } }); // Line 437: ν…Œμ΄λΈ” 라벨 쑰회 await prisma.table_labels.findUnique({ where: { table_name: tableName }, select: { table_name, table_label, description, ... } }); ``` #### 2. column_labels 관리 (5개) ```typescript // Line 323: 컬럼 라벨 UPSERT await prisma.column_labels.upsert({ where: { table_name_column_name: { table_name: tableName, column_name: columnName } }, update: { column_label, input_type, ... }, create: { table_name, column_name, ... } }); // Line 481: 컬럼 라벨 쑰회 await prisma.column_labels.findUnique({ where: { table_name_column_name: { table_name: tableName, column_name: columnName } }, select: { id, table_name, column_name, ... } }); // Line 567: 컬럼 쑴재 확인 await prisma.column_labels.findFirst({ where: { table_name, column_name } }); // Line 586: 컬럼 라벨 μ—…λ°μ΄νŠΈ await prisma.column_labels.update({ where: { id: existingColumn.id }, data: { web_type, detail_settings, ... } }); // Line 610: 컬럼 라벨 생성 await prisma.column_labels.create({ data: { table_name, column_name, web_type, ... } }); // Line 1003: 파일 νƒ€μž… 컬럼 쑰회 await prisma.column_labels.findMany({ where: { table_name, web_type: 'file' }, select: { column_name } }); // Line 1382: 컬럼 μ›Ήνƒ€μž… 정보 쑰회 await prisma.column_labels.findFirst({ where: { table_name, column_name }, select: { web_type, code_category, ... } }); // Line 2690: 컬럼 라벨 UPSERT (볡제) await prisma.column_labels.upsert({ where: { table_name_column_name: { table_name, column_name } }, update: { column_label, web_type, ... }, create: { table_name, column_name, ... } }); ``` #### 3. attach_file_info 관리 (2개) ```typescript // Line 914: 파일 정보 쑰회 await prisma.attach_file_info.findMany({ where: { target_objid, doc_type, status: 'ACTIVE' }, select: { objid, real_file_name, file_size, ... }, orderBy: { regdate: 'desc' } }); // Line 959: 파일 경둜둜 파일 정보 쑰회 await prisma.attach_file_info.findFirst({ where: { file_path, status: 'ACTIVE' }, select: { objid, real_file_name, ... } }); ``` #### 4. νŠΈλžœμž­μ…˜ (1개) ```typescript // Line 391: 전체 컬럼 μ„€μ • 일괄 μ—…λ°μ΄νŠΈ await prisma.$transaction(async (tx) => { await this.insertTableIfNotExists(tableName); for (const columnSetting of columnSettings) { await this.updateColumnSettings(tableName, columnName, columnSetting); } }); ``` --- ## πŸ“ μ „ν™˜ μ˜ˆμ‹œ ### μ˜ˆμ‹œ 1: table_labels UPSERT μ „ν™˜ **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript await prisma.table_labels.upsert({ where: { table_name: tableName }, update: {}, create: { table_name: tableName, table_label: tableName, description: "", }, }); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript import { query } from "../database/db"; await query( `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (table_name) DO NOTHING`, [tableName, tableName, ""] ); ``` ### μ˜ˆμ‹œ 2: column_labels UPSERT μ „ν™˜ **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript await prisma.column_labels.upsert({ where: { table_name_column_name: { table_name: tableName, column_name: columnName, }, }, update: { column_label: settings.columnLabel, input_type: settings.inputType, detail_settings: settings.detailSettings, updated_date: new Date(), }, create: { table_name: tableName, column_name: columnName, column_label: settings.columnLabel, input_type: settings.inputType, detail_settings: settings.detailSettings, }, }); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript await query( `INSERT INTO column_labels ( table_name, column_name, column_label, input_type, detail_settings, code_category, code_value, reference_table, reference_column, display_column, display_order, is_visible, created_date, updated_date ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW()) ON CONFLICT (table_name, column_name) DO UPDATE SET column_label = EXCLUDED.column_label, input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, code_category = EXCLUDED.code_category, code_value = EXCLUDED.code_value, reference_table = EXCLUDED.reference_table, reference_column = EXCLUDED.reference_column, display_column = EXCLUDED.display_column, display_order = EXCLUDED.display_order, is_visible = EXCLUDED.is_visible, updated_date = NOW()`, [ tableName, columnName, settings.columnLabel, settings.inputType, settings.detailSettings, settings.codeCategory, settings.codeValue, settings.referenceTable, settings.referenceColumn, settings.displayColumn, settings.displayOrder || 0, settings.isVisible !== undefined ? settings.isVisible : true, ] ); ``` ### μ˜ˆμ‹œ 3: νŠΈλžœμž­μ…˜ μ „ν™˜ **κΈ°μ‘΄ Prisma μ½”λ“œ:** ```typescript await prisma.$transaction(async (tx) => { await this.insertTableIfNotExists(tableName); for (const columnSetting of columnSettings) { await this.updateColumnSettings(tableName, columnName, columnSetting); } }); ``` **μƒˆλ‘œμš΄ Raw Query μ½”λ“œ:** ```typescript import { transaction } from "../database/db"; await transaction(async (client) => { // ν…Œμ΄λΈ” 라벨 μžλ™ μΆ”κ°€ await client.query( `INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (table_name) DO NOTHING`, [tableName, tableName, ""] ); // 각 컬럼 μ„€μ • μ—…λ°μ΄νŠΈ for (const columnSetting of columnSettings) { const columnName = columnSetting.columnName; if (columnName) { await client.query( `INSERT INTO column_labels (...) VALUES (...) ON CONFLICT (table_name, column_name) DO UPDATE SET ...`, [...] ); } } }); ``` --- ## πŸ§ͺ ν…ŒμŠ€νŠΈ κ³„νš ### λ‹¨μœ„ ν…ŒμŠ€νŠΈ (10개) ```typescript describe("TableManagementService Raw Query μ „ν™˜ ν…ŒμŠ€νŠΈ", () => { describe("insertTableIfNotExists", () => { test("ν…Œμ΄λΈ” 라벨 UPSERT 성곡", async () => { ... }); test("쀑볡 ν…Œμ΄λΈ” 처리", async () => { ... }); }); describe("updateColumnSettings", () => { test("컬럼 μ„€μ • UPSERT 성곡", async () => { ... }); test("κΈ°μ‘΄ 컬럼 μ—…λ°μ΄νŠΈ", async () => { ... }); }); describe("getTableLabels", () => { test("ν…Œμ΄λΈ” 라벨 쑰회 성곡", async () => { ... }); }); describe("getColumnLabels", () => { test("컬럼 라벨 쑰회 성곡", async () => { ... }); }); describe("updateAllColumnSettings", () => { test("일괄 μ—…λ°μ΄νŠΈ 성곡 (νŠΈλžœμž­μ…˜)", async () => { ... }); test("λΆ€λΆ„ μ‹€νŒ¨ μ‹œ λ‘€λ°±", async () => { ... }); }); describe("getFileInfoByColumnAndTarget", () => { test("파일 정보 쑰회 성곡", async () => { ... }); }); }); ``` ### 톡합 ν…ŒμŠ€νŠΈ (5개 μ‹œλ‚˜λ¦¬μ˜€) ```typescript describe("ν…Œμ΄λΈ” 관리 톡합 ν…ŒμŠ€νŠΈ", () => { test("ν…Œμ΄λΈ” 라벨 생성 β†’ 쑰회 β†’ μˆ˜μ •", async () => { ... }); test("컬럼 라벨 생성 β†’ 쑰회 β†’ μˆ˜μ •", async () => { ... }); test("컬럼 일괄 μ„€μ • μ—…λ°μ΄νŠΈ", async () => { ... }); test("파일 정보 쑰회 및 보강", async () => { ... }); test("νŠΈλžœμž­μ…˜ λ‘€λ°± ν…ŒμŠ€νŠΈ", async () => { ... }); }); ``` --- ## πŸ“‹ 체크리슀트 ### 1단계: table_labels μ „ν™˜ (2개 ν•¨μˆ˜) ⏳ **μ§„ν–‰ μ˜ˆμ •** - [ ] `insertTableIfNotExists()` - UPSERT - [ ] `getTableLabels()` - 쑰회 ### 2단계: column_labels μ „ν™˜ (5개 ν•¨μˆ˜) ⏳ **μ§„ν–‰ μ˜ˆμ •** - [ ] `updateColumnSettings()` - UPSERT - [ ] `getColumnLabels()` - 쑰회 - [ ] `updateColumnWebType()` - findFirst + update/create - [ ] `getColumnWebTypeInfo()` - findFirst - [ ] `updateColumnLabel()` - UPSERT (볡제) ### 3단계: attach_file_info μ „ν™˜ (2개 ν•¨μˆ˜) ⏳ **μ§„ν–‰ μ˜ˆμ •** - [ ] `getFileInfoByColumnAndTarget()` - findMany - [ ] `getFileInfoByPath()` - findFirst ### 4단계: νŠΈλžœμž­μ…˜ μ „ν™˜ (1개 ν•¨μˆ˜) ⏳ **μ§„ν–‰ μ˜ˆμ •** - [ ] `updateAllColumnSettings()` - νŠΈλžœμž­μ…˜ ### 5단계: ν…ŒμŠ€νŠΈ & 검증 ⏳ **μ§„ν–‰ μ˜ˆμ •** - [ ] λ‹¨μœ„ ν…ŒμŠ€νŠΈ μž‘μ„± (10개) - [ ] 톡합 ν…ŒμŠ€νŠΈ μž‘μ„± (5개 μ‹œλ‚˜λ¦¬μ˜€) - [ ] Prisma import μ™„μ „ 제거 확인 - [ ] μ„±λŠ₯ ν…ŒμŠ€νŠΈ --- ## 🎯 μ™„λ£Œ κΈ°μ€€ - [ ] **33개 λͺ¨λ“  Prisma ν˜ΈμΆœμ„ Raw Query둜 μ „ν™˜ μ™„λ£Œ** - [ ] 26개 `$queryRaw` β†’ `query()` ν•¨μˆ˜λ‘œ ꡐ체 - [ ] 7개 ORM λ©”μ„œλ“œ β†’ `query()` ν•¨μˆ˜λ‘œ μ „ν™˜ (SQL μž‘μ„±) - [ ] **λͺ¨λ“  TypeScript 컴파일 였λ₯˜ ν•΄κ²°** - [ ] **νŠΈλžœμž­μ…˜ 정상 λ™μž‘ 확인** - [ ] **μ—λŸ¬ 처리 및 λ‘€λ°± 정상 λ™μž‘** - [ ] **λͺ¨λ“  λ‹¨μœ„ ν…ŒμŠ€νŠΈ 톡과 (10개)** - [ ] **λͺ¨λ“  톡합 ν…ŒμŠ€νŠΈ μž‘μ„± μ™„λ£Œ (5개 μ‹œλ‚˜λ¦¬μ˜€)** - [ ] **`import prisma` μ™„μ „ 제거 및 `import { query, transaction } from "../database/db"` μ‚¬μš©** - [ ] **μ„±λŠ₯ μ €ν•˜ μ—†μŒ (κΈ°μ‘΄ λŒ€λΉ„ Β±10% 이내)** --- ## πŸ’‘ νŠΉμ΄μ‚¬ν•­ ### SQL은 이미 λŒ€λΆ€λΆ„ μž‘μ„±λ˜μ–΄ 있음 이 μ„œλΉ„μŠ€λŠ” 이미 79%κ°€ `$queryRaw`λ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ–΄, **SQL μž‘μ„±μ€ μ™„λ£Œ**λ˜μ—ˆμŠ΅λ‹ˆλ‹€: - βœ… `information_schema` 쑰회: SQL μž‘μ„± μ™„λ£Œ (`$queryRaw` μ‚¬μš© 쀑) - βœ… 동적 ν…Œμ΄λΈ” 쿼리: SQL μž‘μ„± μ™„λ£Œ (`$queryRawUnsafe` μ‚¬μš© 쀑) - βœ… DDL μ‹€ν–‰: SQL μž‘μ„± μ™„λ£Œ (`$executeRaw` μ‚¬μš© 쀑) - ⏳ **μ „ν™˜ μž‘μ—…**: `prisma.$queryRaw` β†’ `query()` ν•¨μˆ˜λ‘œ **λ‹¨μˆœ ꡐ체만 ν•„μš”** - ⏳ CRUD μž‘μ—…: 7개만 SQL μƒˆλ‘œ μž‘μ„± ν•„μš” ### UPSERT νŒ¨ν„΄ μ€‘μš” λŒ€λΆ€λΆ„μ˜ μ „ν™˜μ΄ UPSERT νŒ¨ν„΄μ΄λ―€λ‘œ PostgreSQL의 `ON CONFLICT` ꡬ문을 ν™œμš©ν•©λ‹ˆλ‹€. --- **μž‘μ„±μΌ**: 2025-09-30 **μ˜ˆμƒ μ†Œμš” μ‹œκ°„**: 1-1.5일 (SQL은 79% μž‘μ„± μ™„λ£Œ, ν•¨μˆ˜ ꡐ체 μž‘μ—… ν•„μš”) **λ‹΄λ‹Ήμž**: λ°±μ—”λ“œ κ°œλ°œνŒ€ **μš°μ„ μˆœμœ„**: 🟑 쀑간 (Phase 2.2) **μƒνƒœ**: ⏳ **μ§„ν–‰ μ˜ˆμ •** **νŠΉμ΄μ‚¬ν•­**: SQL은 λŒ€λΆ€λΆ„ μž‘μ„±λ˜μ–΄ μžˆμ–΄ `prisma.$queryRaw` β†’ `query()` λ‹¨μˆœ ꡐ체 μž‘μ—…μ΄ μ£Όμš” μž‘μ—