# ๐Ÿ“‹ ํŒŒ์ผ๋ณ„ ์ƒ์„ธ Prisma โ†’ Raw Query ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ## ๐ŸŽฏ ๊ฐœ์š” ์ด 42๊ฐœ ํŒŒ์ผ, 444๊ฐœ Prisma ํ˜ธ์ถœ์„ Raw Query๋กœ ์ „ํ™˜ํ•˜๋Š” ์ƒ์„ธ ๊ณ„ํš์ž…๋‹ˆ๋‹ค. ๊ฐ ํŒŒ์ผ์˜ ๋ณต์žก๋„, ์˜์กด์„ฑ, ์ „ํ™˜ ์ „๋žต์„ ๋ถ„์„ํ•˜์—ฌ ๊ธฐ๋Šฅ ์†์‹ค ์—†๋Š” ์™„์ „ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. --- ## ๐Ÿ”ด Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค ์ „ํ™˜ (107๊ฐœ ํ˜ธ์ถœ) ### 1. screenManagementService.ts (46๊ฐœ ํ˜ธ์ถœ) โญ ์ตœ์šฐ์„  #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋งค์šฐ ๋†’์Œ (JSON ์ฒ˜๋ฆฌ, ๋ณต์žกํ•œ ์กฐ์ธ, ํŠธ๋žœ์žญ์…˜) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ํ™”๋ฉด ์ •์˜ ๊ด€๋ฆฌ, ๋ ˆ์ด์•„์›ƒ ์ €์žฅ/๋ถˆ๋Ÿฌ์˜ค๊ธฐ, ๋ฉ”๋‰ด ํ• ๋‹น - **์˜์กด์„ฑ**: screen_definitions, screen_components, screen_layouts ๋“ฑ #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 1.1 ๊ธฐ๋ณธ CRUD ์ž‘์—… ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ 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 ์ฝ”๋“œ const { query, params } = QueryBuilder.insert( "screen_definitions", { screen_name: screenData.screenName, screen_code: screenData.screenCode, table_name: screenData.tableName, company_code: screenData.companyCode, description: screenData.description, created_by: screenData.createdBy, created_at: new Date(), updated_at: new Date(), }, { returning: ["*"], } ); const [screen] = await DatabaseManager.query(query, params); ``` ##### 1.2 ๋ณต์žกํ•œ ์กฐ์ธ ์ฟผ๋ฆฌ ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ (๋ณต์žกํ•œ include) const screens = await prisma.screen_definitions.findMany({ where: whereClause, include: { screen_components: true, screen_layouts: true, }, orderBy: { created_at: "desc" }, skip: (page - 1) * size, take: size, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const { query, params } = QueryBuilder.select("screen_definitions", { columns: [ "sd.*", "json_agg(DISTINCT sc.*) as screen_components", "json_agg(DISTINCT sl.*) as screen_layouts", ], joins: [ { type: "LEFT", table: "screen_components sc", on: "sd.id = sc.screen_id", }, { type: "LEFT", table: "screen_layouts sl", on: "sd.id = sl.screen_id", }, ], where: whereClause, orderBy: "sd.created_at DESC", limit: size, offset: (page - 1) * size, groupBy: ["sd.id"], }); const screens = await DatabaseManager.query(query, params); ``` ##### 1.3 JSON ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ (JSON ๊ฒ€์ƒ‰) const screens = await prisma.screen_definitions.findMany({ where: { screen_config: { path: ["type"], equals: "form" }, }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const screens = await DatabaseManager.query( ` SELECT * FROM screen_definitions WHERE screen_config->>'type' = $1 `, ["form"] ); ``` ##### 1.4 ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ํŠธ๋žœ์žญ์…˜ await prisma.$transaction(async (tx) => { const screen = await tx.screen_definitions.create({ data: screenData }); await tx.screen_components.createMany({ data: components }); return screen; }); // ์ƒˆ๋กœ์šด Raw Query ํŠธ๋žœ์žญ์…˜ await DatabaseManager.transaction(async (client) => { const screenResult = await client.query( "INSERT INTO screen_definitions (...) VALUES (...) RETURNING *", screenParams ); const screen = screenResult.rows[0]; for (const component of components) { await client.query( "INSERT INTO screen_components (...) VALUES (...)", componentParams ); } return screen; }); ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **๋‹จ์œ„ ํ…Œ์ŠคํŠธ**: ๊ฐ ๋ฉ”์„œ๋“œ๋ณ„ ์ž…์ถœ๋ ฅ ๊ฒ€์ฆ 2. **ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ**: ํ™”๋ฉด ์ƒ์„ฑ โ†’ ์กฐํšŒ โ†’ ์ˆ˜์ • โ†’ ์‚ญ์ œ ์ „์ฒด ํ”Œ๋กœ์šฐ 3. **JSON ๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ**: ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ์ €์žฅ/๋ณต์› 4. **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ**: ๋Œ€๋Ÿ‰ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ๋น„๊ต #### โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ - JSON ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜ ์ฃผ์˜ (PostgreSQL JSONB โ†” JavaScript Object) - ๋‚ ์งœ ํƒ€์ž… ๋ณ€ํ™˜ (Prisma DateTime โ†” PostgreSQL timestamp) - NULL ๊ฐ’ ์ฒ˜๋ฆฌ ์ผ๊ด€์„ฑ ์œ ์ง€ --- ### 2. tableManagementService.ts (35๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋งค์šฐ ๋†’์Œ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ, DDL ์‹คํ–‰, ๋™์  ์ฟผ๋ฆฌ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ •๋ณด ๊ด€๋ฆฌ, ๋™์  ํ…Œ์ด๋ธ” ์ƒ์„ฑ, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์บ์‹ฑ - **์˜์กด์„ฑ**: information_schema, table_labels, column_labels #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 2.1 ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ (์ด๋ฏธ Raw Query ์‚ฌ์šฉ ์ค‘) ```typescript // ํ˜„์žฌ ์ฝ”๋“œ (์ด๋ฏธ Raw Query) const rawTables = await prisma.$queryRaw` SELECT t.table_name as "tableName", COALESCE(tl.table_label, t.table_name) as "displayName" FROM information_schema.tables t LEFT JOIN table_labels tl ON t.table_name = tl.table_name WHERE t.table_schema = 'public' `; // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ (Prisma ์ œ๊ฑฐ) const rawTables = await DatabaseManager.query(` SELECT t.table_name as "tableName", COALESCE(tl.table_label, t.table_name) as "displayName" FROM information_schema.tables t LEFT JOIN table_labels tl ON t.table_name = tl.table_name WHERE t.table_schema = 'public' `); ``` ##### 2.2 ๋™์  ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์กฐํšŒ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ (๋™์  ํ…Œ์ด๋ธ”๋ช…) const data = await prisma.$queryRawUnsafe( `SELECT * FROM ${tableName} WHERE id = $1`, [id] ); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const data = await DatabaseManager.queryUnsafe( `SELECT * FROM ${DatabaseValidator.sanitizeTableName( tableName )} WHERE id = $1`, [id] ); ``` ##### 2.3 UPSERT ์ž‘์—… ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma UPSERT await prisma.table_labels.upsert({ where: { table_name: tableName }, update: { table_label: label, updated_date: new Date() }, create: { table_name: tableName, table_label: label, created_date: new Date(), }, }); // ์ƒˆ๋กœ์šด Raw Query UPSERT const { query, params } = QueryBuilder.insert( "table_labels", { table_name: tableName, table_label: label, created_date: new Date(), updated_date: new Date(), }, { onConflict: { columns: ["table_name"], action: "DO UPDATE", updateSet: ["table_label", "updated_date"], }, returning: ["*"], } ); await DatabaseManager.query(query, params); ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…Œ์ŠคํŠธ**: information_schema ์กฐํšŒ ๊ฒฐ๊ณผ ์ผ์น˜์„ฑ 2. **๋™์  ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ ํ…Œ์ด๋ธ”๋ช…/์ปฌ๋Ÿผ๋ช…์œผ๋กœ ์•ˆ์ „์„ฑ ๊ฒ€์ฆ 3. **DDL ํ…Œ์ŠคํŠธ**: ํ…Œ์ด๋ธ” ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ๊ธฐ๋Šฅ 4. **์บ์‹œ ํ…Œ์ŠคํŠธ**: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์บ์‹ฑ ๋™์ž‘ ๊ฒ€์ฆ --- ### 3. dataflowService.ts (31๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋†’์Œ (๊ด€๊ณ„ ๊ด€๋ฆฌ, ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ๊ด€๋ฆฌ, ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ - **์˜์กด์„ฑ**: table_relationships, dataflow_diagrams #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 3.1 ๊ด€๊ณ„ ์ƒ์„ฑ ๋กœ์ง ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const maxDiagramId = await prisma.table_relationships.findFirst({ where: { company_code: data.companyCode }, orderBy: { diagram_id: "desc" }, select: { diagram_id: true }, }); const relationship = await prisma.table_relationships.create({ data: { diagram_id: diagramId, relationship_name: data.relationshipName, // ... ๊ธฐํƒ€ ํ•„๋“œ }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const maxDiagramResult = await DatabaseManager.query( ` SELECT diagram_id FROM table_relationships WHERE company_code = $1 ORDER BY diagram_id DESC LIMIT 1 `, [data.companyCode] ); const diagramId = (maxDiagramResult[0]?.diagram_id || 0) + 1; const { query, params } = QueryBuilder.insert( "table_relationships", { diagram_id: diagramId, relationship_name: data.relationshipName, // ... ๊ธฐํƒ€ ํ•„๋“œ }, { returning: ["*"] } ); const [relationship] = await DatabaseManager.query(query, params); ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **๊ด€๊ณ„ ์ƒ์„ฑ ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ํŒจํ„ด 2. **์ค‘๋ณต ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ**: ๋™์ผ ๊ด€๊ณ„ ์ƒ์„ฑ ๋ฐฉ์ง€ 3. **๋‹ค์ด์–ด๊ทธ๋žจ ํ…Œ์ŠคํŠธ**: ๋ณต์žกํ•œ ๊ด€๊ณ„๋„ ์ƒ์„ฑ/์กฐํšŒ --- ## ๐ŸŸก Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ „ํ™˜ (162๊ฐœ ํ˜ธ์ถœ) ### 4. multilangService.ts (25๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋†’์Œ (์žฌ๊ท€ ์ฟผ๋ฆฌ, ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๋‹ค๊ตญ์–ด ๋ฒˆ์—ญ ๊ด€๋ฆฌ, ๊ณ„์ธต ๊ตฌ์กฐ ์ฒ˜๋ฆฌ - **์˜์กด์„ฑ**: multilang_translations, ์žฌ๊ท€ ๊ด€๊ณ„ #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 4.1 ์žฌ๊ท€ ์ฟผ๋ฆฌ ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ (์žฌ๊ท€ ๊ด€๊ณ„ ์กฐํšŒ) const translations = await prisma.multilang_translations.findMany({ where: { parent_id: null }, include: { children: { include: { children: true, // ์ค‘์ฒฉ include }, }, }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ (WITH RECURSIVE) const translations = await DatabaseManager.query(` WITH RECURSIVE translation_tree AS ( SELECT *, 0 as level FROM multilang_translations WHERE parent_id IS NULL UNION ALL SELECT t.*, tt.level + 1 FROM multilang_translations t JOIN translation_tree tt ON t.parent_id = tt.id ) SELECT * FROM translation_tree ORDER BY level, sort_order `); ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **์žฌ๊ท€ ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ**: ๊นŠ์€ ๊ณ„์ธต ๊ตฌ์กฐ ์ฒ˜๋ฆฌ 2. **๋‹ค๊ตญ์–ด ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ ์–ธ์–ด ์ฝ”๋“œ ์ฒ˜๋ฆฌ 3. **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ**: ๋Œ€๋Ÿ‰ ๋ฒˆ์—ญ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ --- ### 5. batchService.ts (16๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ์ค‘๊ฐ„ (๋ฐฐ์น˜ ์ž‘์—… ๊ด€๋ฆฌ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๋ฐฐ์น˜ ์ž‘์—… ์Šค์ผ€์ค„๋ง, ์‹คํ–‰ ์ด๋ ฅ ๊ด€๋ฆฌ - **์˜์กด์„ฑ**: batch_configs, batch_execution_logs #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 5.1 ๋ฐฐ์น˜ ์„ค์ • ๊ด€๋ฆฌ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const batchConfigs = await prisma.batch_configs.findMany({ where: { is_active: true }, include: { execution_logs: { take: 10, orderBy: { created_at: "desc" } } }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const batchConfigs = await DatabaseManager.query(` SELECT bc.*, json_agg( json_build_object( 'id', bel.id, 'status', bel.status, 'created_at', bel.created_at ) ORDER BY bel.created_at DESC ) FILTER (WHERE bel.id IS NOT NULL) as execution_logs FROM batch_configs bc LEFT JOIN ( SELECT DISTINCT ON (batch_config_id) batch_config_id, id, status, created_at, ROW_NUMBER() OVER (PARTITION BY batch_config_id ORDER BY created_at DESC) as rn FROM batch_execution_logs ) bel ON bc.id = bel.batch_config_id AND bel.rn <= 10 WHERE bc.is_active = true GROUP BY bc.id `); ``` --- ### 7. dynamicFormService.ts (15๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋†’์Œ (UPSERT, ๋™์  ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ, ํƒ€์ž… ๋ณ€ํ™˜) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๋™์  ํผ ๋ฐ์ดํ„ฐ ์ €์žฅ/์กฐํšŒ, ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ, ํƒ€์ž… ๋ณ€ํ™˜ - **์˜์กด์„ฑ**: ๋™์  ํ…Œ์ด๋ธ”๋“ค, form_data, ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 7.1 ๋™์  UPSERT ๋กœ์ง ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ (๋™์  ํ…Œ์ด๋ธ” UPSERT) const existingRecord = await prisma.$queryRawUnsafe( `SELECT * FROM ${tableName} WHERE id = $1`, [id] ); if (existingRecord.length > 0) { await prisma.$executeRawUnsafe(updateQuery, updateValues); } else { await prisma.$executeRawUnsafe(insertQuery, insertValues); } // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ (PostgreSQL UPSERT) const upsertQuery = ` INSERT INTO ${DatabaseValidator.sanitizeTableName(tableName)} (${columns.join(", ")}) VALUES (${placeholders.join(", ")}) ON CONFLICT (id) DO UPDATE SET ${updateColumns.map((col) => `${col} = EXCLUDED.${col}`).join(", ")}, updated_at = NOW() RETURNING * `; const [result] = await DatabaseManager.query(upsertQuery, values); ``` ##### 7.2 ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง ๊ฐ•ํ™” ```typescript // ๊ธฐ์กด ํƒ€์ž… ๋ณ€ํ™˜ (Prisma ์ž๋™ ์ฒ˜๋ฆฌ) const data = await prisma.someTable.create({ data: formData }); // ์ƒˆ๋กœ์šด ํƒ€์ž… ๋ณ€ํ™˜ (๋ช…์‹œ์  ์ฒ˜๋ฆฌ) const convertedData = this.convertFormDataForPostgreSQL(formData, tableSchema); const { query, params } = QueryBuilder.insert(tableName, convertedData, { returning: ["*"] }); const [result] = await DatabaseManager.query(query, params); // ํƒ€์ž… ๋ณ€ํ™˜ ํ•จ์ˆ˜ ๊ฐ•ํ™” private convertFormDataForPostgreSQL(data: any, schema: TableColumn[]): any { const converted = {}; for (const [key, value] of Object.entries(data)) { const column = schema.find(col => col.columnName === key); if (column) { converted[key] = this.convertValueByType(value, column.dataType); } } return converted; } ``` ##### 7.3 ๋™์  ๊ฒ€์ฆ ๋กœ์ง ```typescript // ๊ธฐ์กด Prisma ๊ฒ€์ฆ (์Šคํ‚ค๋งˆ ๊ธฐ๋ฐ˜) const validation = await prisma.$validator.validate(data, schema); // ์ƒˆ๋กœ์šด Raw Query ๊ฒ€์ฆ async validateFormData(data: any, tableName: string): Promise { // ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์กฐํšŒ const schema = await this.getTableSchema(tableName); const errors: ValidationError[] = []; for (const column of schema) { const value = data[column.columnName]; // NULL ๊ฒ€์ฆ if (!column.nullable && (value === null || value === undefined)) { errors.push({ field: column.columnName, message: `${column.columnName}์€(๋Š”) ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.`, code: 'REQUIRED_FIELD' }); } // ํƒ€์ž… ๊ฒ€์ฆ if (value !== null && !this.isValidType(value, column.dataType)) { errors.push({ field: column.columnName, message: `${column.columnName}์˜ ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`, code: 'INVALID_TYPE' }); } } return { valid: errors.length === 0, errors }; } ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **UPSERT ํ…Œ์ŠคํŠธ**: ์‹ ๊ทœ ์ƒ์„ฑ vs ๊ธฐ์กด ์—…๋ฐ์ดํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค 2. **ํƒ€์ž… ๋ณ€ํ™˜ ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ PostgreSQL ํƒ€์ž… ๋ณ€ํ™˜ 3. **๊ฒ€์ฆ ํ…Œ์ŠคํŠธ**: ํ•„์ˆ˜ ํ•„๋“œ, ํƒ€์ž… ๊ฒ€์ฆ, ๊ธธ์ด ์ œํ•œ 4. **๋™์  ํ…Œ์ด๋ธ” ํ…Œ์ŠคํŠธ**: ๋Ÿฐํƒ€์ž„์— ์ƒ์„ฑ๋œ ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ --- ### 8. externalDbConnectionService.ts (15๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ๋†’์Œ (๋‹ค์ค‘ DB ์—ฐ๊ฒฐ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—ฐ๋™) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๊ด€๋ฆฌ, ์Šคํ‚ค๋งˆ ๋™๊ธฐํ™” - **์˜์กด์„ฑ**: external_db_connections, connection_pools #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 8.1 ์—ฐ๊ฒฐ ์„ค์ • ๊ด€๋ฆฌ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const connections = await prisma.external_db_connections.findMany({ where: { is_active: true }, include: { connection_pools: true }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const connections = await DatabaseManager.query(` SELECT edc.*, json_agg(cp.*) as connection_pools FROM external_db_connections edc LEFT JOIN connection_pools cp ON edc.id = cp.connection_id WHERE edc.is_active = true GROUP BY edc.id `); ``` ##### 8.2 ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ ```typescript // ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ class ExternalConnectionManager { private static pools = new Map(); static async getConnection(connectionId: string): Promise { if (!this.pools.has(connectionId)) { const config = await this.getConnectionConfig(connectionId); this.pools.set(connectionId, new Pool(config)); } return this.pools.get(connectionId)!.connect(); } private static async getConnectionConfig(connectionId: string) { const [config] = await DatabaseManager.query( ` SELECT host, port, database, username, password, ssl_config FROM external_db_connections WHERE id = $1 AND is_active = true `, [connectionId] ); return { host: config.host, port: config.port, database: config.database, user: config.username, password: EncryptUtil.decrypt(config.password), ssl: config.ssl_config, }; } } ``` --- ## ๐ŸŸข Phase 4: ํ™•์žฅ ๊ธฐ๋Šฅ ์ „ํ™˜ (129๊ฐœ ํ˜ธ์ถœ) ### 9. adminController.ts (28๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ์ค‘๊ฐ„ (์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด, ๋‹ค์–‘ํ•œ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด, ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ, ๊ถŒํ•œ ๊ด€๋ฆฌ, ์‹œ์Šคํ…œ ์„ค์ • - **์˜์กด์„ฑ**: user_info, menu_info, auth_groups, company_mng #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 9.1 ๋ฉ”๋‰ด ๊ด€๋ฆฌ API ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ export async function getAdminMenus(req: AuthenticatedRequest, res: Response) { const menus = await prisma.menu_info.findMany({ where: { is_active: "Y", company_code: userCompanyCode, }, include: { parent: true, children: { where: { is_active: "Y" } }, }, orderBy: { sort_order: "asc" }, }); res.json({ success: true, data: menus }); } // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ export async function getAdminMenus(req: AuthenticatedRequest, res: Response) { // ๊ณ„์ธตํ˜• ๋ฉ”๋‰ด ๊ตฌ์กฐ๋ฅผ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ์กฐํšŒ const menus = await DatabaseManager.query( ` WITH RECURSIVE menu_tree AS ( SELECT m.*, 0 as level, ARRAY[m.sort_order] as path FROM menu_info m WHERE m.parent_id IS NULL AND m.is_active = 'Y' AND m.company_code = $1 UNION ALL SELECT m.*, mt.level + 1, mt.path || m.sort_order FROM menu_info m JOIN menu_tree mt ON m.parent_id = mt.id WHERE m.is_active = 'Y' ) SELECT * FROM menu_tree ORDER BY path `, [userCompanyCode] ); res.json({ success: true, data: menus }); } ``` ##### 9.2 ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ API ์ „ํ™˜ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ export async function getUserList(req: AuthenticatedRequest, res: Response) { const users = await prisma.user_info.findMany({ where: { company_code: userCompanyCode }, include: { dept_info: true, user_auth: { include: { auth_group: true } }, }, }); } // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ export async function getUserList(req: AuthenticatedRequest, res: Response) { const users = await DatabaseManager.query( ` SELECT ui.*, di.dept_name, json_agg( json_build_object( 'auth_code', ag.auth_code, 'auth_name', ag.auth_name ) ) FILTER (WHERE ag.auth_code IS NOT NULL) as authorities FROM user_info ui LEFT JOIN dept_info di ON ui.dept_code = di.dept_code LEFT JOIN user_auth ua ON ui.user_id = ua.user_id LEFT JOIN auth_group ag ON ua.auth_code = ag.auth_code WHERE ui.company_code = $1 GROUP BY ui.user_id, di.dept_name ORDER BY ui.created_date DESC `, [userCompanyCode] ); } ``` ##### 9.3 ๊ถŒํ•œ ๊ด€๋ฆฌ API ์ „ํ™˜ ```typescript // ๋ณต์žกํ•œ ๊ถŒํ•œ ์ฒดํฌ ๋กœ์ง export async function checkUserPermission( req: AuthenticatedRequest, res: Response ) { const { menuUrl } = req.body; const hasPermission = await DatabaseManager.query( ` SELECT EXISTS ( SELECT 1 FROM user_auth ua JOIN auth_group ag ON ua.auth_code = ag.auth_code JOIN menu_auth_group mag ON ag.auth_code = mag.auth_code JOIN menu_info mi ON mag.menu_id = mi.id WHERE ua.user_id = $1 AND mi.url = $2 AND ua.is_active = 'Y' AND ag.is_active = 'Y' AND mi.is_active = 'Y' ) as has_permission `, [req.user.userId, menuUrl] ); res.json({ success: true, hasPermission: hasPermission[0].has_permission }); } ``` #### ๐Ÿงช ํ…Œ์ŠคํŠธ ์ „๋žต 1. **API ์‘๋‹ต ํ…Œ์ŠคํŠธ**: ๊ธฐ์กด API์™€ ๋™์ผํ•œ ์‘๋‹ต ๊ตฌ์กฐ ํ™•์ธ 2. **๊ถŒํ•œ ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค 3. **๊ณ„์ธต ๊ตฌ์กฐ ํ…Œ์ŠคํŠธ**: ๋ฉ”๋‰ด ํŠธ๋ฆฌ ๊ตฌ์กฐ ์ •ํ™•์„ฑ 4. **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ**: ๋ณต์žกํ•œ ์กฐ์ธ ์ฟผ๋ฆฌ ์„ฑ๋Šฅ --- ### 10. componentStandardService.ts (16๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ์ค‘๊ฐ„ (์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ๊ด€๋ฆฌ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: UI ์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ์ •์˜, ํ…œํ”Œ๋ฆฟ ๊ด€๋ฆฌ - **์˜์กด์„ฑ**: component_standards, ui_templates #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 10.1 ์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ์กฐํšŒ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const components = await prisma.component_standards.findMany({ where: { category: category }, include: { templates: { where: { is_active: true } }, properties: true, }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const components = await DatabaseManager.query( ` SELECT cs.*, json_agg( DISTINCT jsonb_build_object( 'id', ut.id, 'template_name', ut.template_name, 'template_config', ut.template_config ) ) FILTER (WHERE ut.id IS NOT NULL) as templates, json_agg( DISTINCT jsonb_build_object( 'property_name', cp.property_name, 'property_type', cp.property_type, 'default_value', cp.default_value ) ) FILTER (WHERE cp.id IS NOT NULL) as properties FROM component_standards cs LEFT JOIN ui_templates ut ON cs.id = ut.component_id AND ut.is_active = true LEFT JOIN component_properties cp ON cs.id = cp.component_id WHERE cs.category = $1 GROUP BY cs.id `, [category] ); ``` --- ### 11. commonCodeService.ts (15๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ์ค‘๊ฐ„ (์ฝ”๋“œ ๊ด€๋ฆฌ, ๊ณ„์ธต ๊ตฌ์กฐ) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๊ณตํ†ต ์ฝ”๋“œ ๊ด€๋ฆฌ, ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ - **์˜์กด์„ฑ**: code_info, code_category #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 11.1 ๊ณ„์ธตํ˜• ์ฝ”๋“œ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const codes = await prisma.code_info.findMany({ where: { category_code: categoryCode }, include: { parent: true, children: { where: { is_active: "Y" } }, }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ (์žฌ๊ท€ CTE ์‚ฌ์šฉ) const codes = await DatabaseManager.query( ` WITH RECURSIVE code_tree AS ( SELECT ci.*, 0 as level, CAST(ci.sort_order as TEXT) as path FROM code_info ci WHERE ci.parent_code IS NULL AND ci.category_code = $1 AND ci.is_active = 'Y' UNION ALL SELECT ci.*, ct.level + 1, ct.path || '.' || ci.sort_order FROM code_info ci JOIN code_tree ct ON ci.parent_code = ct.code WHERE ci.is_active = 'Y' ) SELECT * FROM code_tree ORDER BY path `, [categoryCode] ); ``` --- ### 12. batchService.ts (16๊ฐœ ํ˜ธ์ถœ) #### ๐Ÿ“Š ํ˜„์žฌ ์ƒํƒœ ๋ถ„์„ - **๋ณต์žก๋„**: ์ค‘๊ฐ„-๋†’์Œ (๋ฐฐ์น˜ ์ž‘์—…, ์Šค์ผ€์ค„๋ง) - **์ฃผ์š” ๊ธฐ๋Šฅ**: ๋ฐฐ์น˜ ์ž‘์—… ๊ด€๋ฆฌ, ์‹คํ–‰ ์ด๋ ฅ, ์Šค์ผ€์ค„๋ง - **์˜์กด์„ฑ**: batch_configs, batch_execution_logs #### ๐Ÿ”ง ์ „ํ™˜ ์ „๋žต ##### 12.1 ๋ฐฐ์น˜ ์‹คํ–‰ ์ด๋ ฅ ๊ด€๋ฆฌ ```typescript // ๊ธฐ์กด Prisma ์ฝ”๋“œ const batchHistory = await prisma.batch_execution_logs.findMany({ where: { batch_config_id: configId }, include: { batch_config: true }, orderBy: { created_at: "desc" }, take: 50, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ const batchHistory = await DatabaseManager.query( ` SELECT bel.*, bc.batch_name, bc.description as batch_description FROM batch_execution_logs bel JOIN batch_configs bc ON bel.batch_config_id = bc.id WHERE bel.batch_config_id = $1 ORDER BY bel.created_at DESC LIMIT 50 `, [configId] ); ``` ##### 12.2 ๋ฐฐ์น˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ```typescript // ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•œ ๋ฐฐ์น˜ ์ƒํƒœ ๊ด€๋ฆฌ async updateBatchStatus(batchId: number, status: string, result?: any) { await DatabaseManager.transaction(async (client) => { // ์‹คํ–‰ ๋กœ๊ทธ ์—…๋ฐ์ดํŠธ await client.query(` UPDATE batch_execution_logs SET status = $1, result = $2, completed_at = NOW(), updated_at = NOW() WHERE id = $3 `, [status, result, batchId]); // ๋ฐฐ์น˜ ์„ค์ •์˜ ๋งˆ์ง€๋ง‰ ์‹คํ–‰ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ await client.query(` UPDATE batch_configs SET last_executed_at = NOW(), last_status = $1, updated_at = NOW() WHERE id = ( SELECT batch_config_id FROM batch_execution_logs WHERE id = $2 ) `, [status, batchId]); }); } ``` --- ## ๐Ÿ“‹ ๋‚˜๋จธ์ง€ ํŒŒ์ผ๋“ค ์š”์•ฝ ์ „ํ™˜ ๊ณ„ํš ### Phase 2 ๋‚˜๋จธ์ง€ ํŒŒ์ผ๋“ค (6๊ฐœ ํ˜ธ์ถœ) - **dataflowControlService.ts** (6๊ฐœ): ์ œ์–ด ๋กœ์ง, ์กฐ๊ฑด๋ถ€ ์‹คํ–‰ - **ddlExecutionService.ts** (6๊ฐœ): DDL ์‹คํ–‰, ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ - **authService.ts** (5๊ฐœ): ์‚ฌ์šฉ์ž ์ธ์ฆ, ํ† ํฐ ๊ด€๋ฆฌ - **multiConnectionQueryService.ts** (4๊ฐœ): ๋‹ค์ค‘ DB ์—ฐ๊ฒฐ ### Phase 3 ๋‚˜๋จธ์ง€ ํŒŒ์ผ๋“ค (121๊ฐœ ํ˜ธ์ถœ) - **dataflowDiagramService.ts** (12๊ฐœ): ๋‹ค์ด์–ด๊ทธ๋žจ ๊ด€๋ฆฌ, JSON ์ฒ˜๋ฆฌ - **collectionService.ts** (11๊ฐœ): ์ปฌ๋ ‰์…˜ ๊ด€๋ฆฌ - **layoutService.ts** (10๊ฐœ): ๋ ˆ์ด์•„์›ƒ ๊ด€๋ฆฌ - **dbTypeCategoryService.ts** (10๊ฐœ): DB ํƒ€์ž… ๋ถ„๋ฅ˜ - **templateStandardService.ts** (9๊ฐœ): ํ…œํ”Œ๋ฆฟ ํ‘œ์ค€ - **ddlAuditLogger.ts** (8๊ฐœ): DDL ๊ฐ์‚ฌ ๋กœ๊ทธ - **externalCallConfigService.ts** (8๊ฐœ): ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • - **batchExternalDbService.ts** (8๊ฐœ): ๋ฐฐ์น˜ ์™ธ๋ถ€DB - **batchExecutionLogService.ts** (7๊ฐœ): ๋ฐฐ์น˜ ์‹คํ–‰ ๋กœ๊ทธ - **eventTriggerService.ts** (6๊ฐœ): ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ - **enhancedDynamicFormService.ts** (6๊ฐœ): ํ™•์žฅ ๋™์  ํผ - **entityJoinService.ts** (5๊ฐœ): ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ - **dataMappingService.ts** (5๊ฐœ): ๋ฐ์ดํ„ฐ ๋งคํ•‘ - **batchManagementService.ts** (5๊ฐœ): ๋ฐฐ์น˜ ๊ด€๋ฆฌ - **batchSchedulerService.ts** (4๊ฐœ): ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ - **dataService.ts** (4๊ฐœ): ๋ฐ์ดํ„ฐ ์„œ๋น„์Šค - **adminService.ts** (3๊ฐœ): ๊ด€๋ฆฌ์ž ์„œ๋น„์Šค - **referenceCacheService.ts** (3๊ฐœ): ์ฐธ์กฐ ์บ์‹œ ### Phase 4 ๋‚˜๋จธ์ง€ ํŒŒ์ผ๋“ค (101๊ฐœ ํ˜ธ์ถœ) - **webTypeStandardController.ts** (11๊ฐœ): ์›นํƒ€์ž… ํ‘œ์ค€ ์ปจํŠธ๋กค๋Ÿฌ - **fileController.ts** (11๊ฐœ): ํŒŒ์ผ ์—…๋กœ๋“œ/๋‹ค์šด๋กœ๋“œ ์ปจํŠธ๋กค๋Ÿฌ - **buttonActionStandardController.ts** (11๊ฐœ): ๋ฒ„ํŠผ ์•ก์…˜ ํ‘œ์ค€ - **entityReferenceController.ts** (4๊ฐœ): ์—”ํ‹ฐํ‹ฐ ์ฐธ์กฐ ์ปจํŠธ๋กค๋Ÿฌ - **database.ts** (4๊ฐœ): ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • - **dataflowExecutionController.ts** (3๊ฐœ): ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ - **screenFileController.ts** (2๊ฐœ): ํ™”๋ฉด ํŒŒ์ผ ์ปจํŠธ๋กค๋Ÿฌ - **ddlRoutes.ts** (2๊ฐœ): DDL ๋ผ์šฐํŠธ - **companyManagementRoutes.ts** (2๊ฐœ): ํšŒ์‚ฌ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ --- ## ๐Ÿ”— ํŒŒ์ผ ๊ฐ„ ์˜์กด์„ฑ ๋ถ„์„ ### 1. ํ•ต์‹ฌ ์˜์กด์„ฑ ์ฒด์ธ ``` DatabaseManager (๊ธฐ๋ฐ˜) โ†“ QueryBuilder (์ฟผ๋ฆฌ ์ƒ์„ฑ) โ†“ Services (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง) โ†“ Controllers (API ์—”๋“œํฌ์ธํŠธ) โ†“ Routes (๋ผ์šฐํŒ…) ``` ### 2. ์„œ๋น„์Šค ๊ฐ„ ์˜์กด์„ฑ ``` tableManagementService โ†“ (ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ) screenManagementService โ†“ (ํ™”๋ฉด ์ •์˜) dynamicFormService โ†“ (ํผ ๋ฐ์ดํ„ฐ) dataflowControlService ``` ### 3. ์ „ํ™˜ ์ˆœ์„œ (์˜์กด์„ฑ ๊ณ ๋ ค) 1. **๊ธฐ๋ฐ˜ ๊ตฌ์กฐ**: DatabaseManager, QueryBuilder 2. **๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„œ๋น„์Šค**: tableManagementService 3. **ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค**: screenManagementService, dataflowService 4. **ํผ ์ฒ˜๋ฆฌ**: dynamicFormService 5. **์™ธ๋ถ€ ์—ฐ๋™**: externalDbConnectionService 6. **๊ด€๋ฆฌ ๊ธฐ๋Šฅ**: adminService, commonCodeService 7. **๋ฐฐ์น˜ ์‹œ์Šคํ…œ**: batchService ๊ณ„์—ด 8. **์ปจํŠธ๋กค๋Ÿฌ**: adminController ๋“ฑ 9. **๋ผ์šฐํŠธ**: ๊ฐ์ข… ๋ผ์šฐํŠธ ํŒŒ์ผ๋“ค --- ## โšก ์ „ํ™˜ ๊ฐ€์†ํ™” ์ „๋žต ### 1. ๋ณ‘๋ ฌ ์ „ํ™˜ ๊ฐ€๋Šฅ ๊ทธ๋ฃน ``` ๊ทธ๋ฃน A (๋…๋ฆฝ์ ): authService, adminService, commonCodeService ๊ทธ๋ฃน B (๋ฐฐ์น˜ ๊ด€๋ จ): batchService, batchSchedulerService, batchExecutionLogService ๊ทธ๋ฃน C (์ปจํŠธ๋กค๋Ÿฌ): adminController, fileController, webTypeStandardController ๊ทธ๋ฃน D (ํ‘œ์ค€ ๊ด€๋ฆฌ): componentStandardService, templateStandardService ``` ### 2. ๊ณตํ†ต ํŒจํ„ด ํ…œํ”Œ๋ฆฟ ํ™œ์šฉ ```typescript // ํ‘œ์ค€ CRUD ํ…œํ”Œ๋ฆฟ class StandardCRUDTemplate { static async create(tableName: string, data: any) { const { query, params } = QueryBuilder.insert(tableName, data, { returning: ["*"], }); return await DatabaseManager.query(query, params); } static async findMany(tableName: string, options: any) { const { query, params } = QueryBuilder.select(tableName, options); return await DatabaseManager.query(query, params); } // ... ๊ธฐํƒ€ ํ‘œ์ค€ ๋ฉ”์„œ๋“œ๋“ค } ``` ### 3. ์ž๋™ํ™” ๋„๊ตฌ ํ™œ์šฉ ```typescript // Prisma โ†’ Raw Query ์ž๋™ ๋ณ€ํ™˜ ๋„๊ตฌ class PrismaToRawConverter { static convertFindMany(prismaCall: string): string { // Prisma findMany ํ˜ธ์ถœ์„ Raw Query๋กœ ์ž๋™ ๋ณ€ํ™˜ } static convertCreate(prismaCall: string): string { // Prisma create ํ˜ธ์ถœ์„ Raw Query๋กœ ์ž๋™ ๋ณ€ํ™˜ } } ``` --- ## ๐Ÿงช ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ „๋žต ### 1. ํŒŒ์ผ๋ณ„ ํ…Œ์ŠคํŠธ ๋งคํŠธ๋ฆญ์Šค | ํŒŒ์ผ๋ช… | ๋‹จ์œ„ํ…Œ์ŠคํŠธ | ํ†ตํ•ฉํ…Œ์ŠคํŠธ | ์„ฑ๋Šฅํ…Œ์ŠคํŠธ | E2Eํ…Œ์ŠคํŠธ | | ----------------------- | ---------- | ---------- | ---------- | --------- | | screenManagementService | โœ… | โœ… | โœ… | โœ… | | tableManagementService | โœ… | โœ… | โœ… | โŒ | | dataflowService | โœ… | โœ… | โŒ | โŒ | | adminController | โœ… | โœ… | โŒ | โœ… | ### 2. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ์ž๋™ํ™” ```typescript // ๊ธฐ์กด Prisma vs ์ƒˆ๋กœ์šด Raw Query ๊ฒฐ๊ณผ ๋น„๊ต describe("Migration Regression Tests", () => { test("screenManagementService.getScreens should return identical results", async () => { const prismaResult = await oldScreenService.getScreens(params); const rawQueryResult = await newScreenService.getScreens(params); expect(normalizeResult(rawQueryResult)).toEqual( normalizeResult(prismaResult) ); }); }); ``` ### 3. ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ ```typescript // ์„ฑ๋Šฅ ๋น„๊ต ํ…Œ์ŠคํŠธ describe("Performance Benchmarks", () => { test("Complex query performance comparison", async () => { const iterations = 1000; const prismaTime = await measureTime( () => prismaService.complexQuery(params), iterations ); const rawQueryTime = await measureTime( () => rawQueryService.complexQuery(params), iterations ); expect(rawQueryTime).toBeLessThanOrEqual(prismaTime * 1.1); // 10% ํ—ˆ์šฉ }); }); ``` --- ## ๐Ÿ”ง ๊ณตํ†ต ์ „ํ™˜ ํŒจํ„ด ### 1. ๊ธฐ๋ณธ CRUD ํŒจํ„ด ```typescript // CREATE const { query, params } = QueryBuilder.insert(tableName, data, { returning: ["*"], }); const [result] = await DatabaseManager.query(query, params); // READ const { query, params } = QueryBuilder.select(tableName, { where, orderBy, limit, }); const results = await DatabaseManager.query(query, params); // UPDATE const { query, params } = QueryBuilder.update(tableName, data, where); const [result] = await DatabaseManager.query(query, params); // DELETE const { query, params } = QueryBuilder.delete(tableName, where); const results = await DatabaseManager.query(query, params); ``` ### 2. ํŠธ๋žœ์žญ์…˜ ํŒจํ„ด ```typescript await DatabaseManager.transaction(async (client) => { const result1 = await client.query(query1, params1); const result2 = await client.query(query2, params2); return { result1, result2 }; }); ``` ### 3. ๋™์  ์ฟผ๋ฆฌ ํŒจํ„ด ```typescript const tableName = DatabaseValidator.sanitizeTableName(userInput); const columnName = DatabaseValidator.sanitizeColumnName(userInput); const query = `SELECT ${columnName} FROM ${tableName} WHERE id = $1`; const result = await DatabaseManager.query(query, [id]); ``` --- ## ๐Ÿ“‹ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### ๊ฐ ํŒŒ์ผ๋ณ„ ํ•„์ˆ˜ ํ™•์ธ์‚ฌํ•ญ - [ ] ๋ชจ๋“  Prisma ํ˜ธ์ถœ ์‹๋ณ„ ๋ฐ ๋ณ€ํ™˜ - [ ] ํƒ€์ž… ๋ณ€ํ™˜ (Date, JSON, BigInt) ์ฒ˜๋ฆฌ - [ ] NULL ๊ฐ’ ์ฒ˜๋ฆฌ ์ผ๊ด€์„ฑ - [ ] ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์œ ์ง€ - [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง ๋ณด์กด - [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” (์ธ๋ฑ์Šค ํžŒํŠธ ๋“ฑ) - [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ - [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰ - [ ] ๊ธฐ๋Šฅ ๋™์ž‘ ๊ฒ€์ฆ - [ ] ์„ฑ๋Šฅ ๋น„๊ต ํ…Œ์ŠคํŠธ ### ๊ณตํ†ต ์ฃผ์˜์‚ฌํ•ญ 1. **SQL ์ธ์ ์…˜ ๋ฐฉ์ง€**: ๋ชจ๋“  ๋™์  ์ฟผ๋ฆฌ์— ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ 2. **ํƒ€์ž… ์•ˆ์ „์„ฑ**: TypeScript ํƒ€์ž… ์ •์˜ ์œ ์ง€ 3. **์—๋Ÿฌ ์ฒ˜๋ฆฌ**: Prisma ์—๋Ÿฌ๋ฅผ ์ ์ ˆํ•œ HTTP ์ƒํƒœ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜ 4. **๋กœ๊น…**: ์ฟผ๋ฆฌ ์‹คํ–‰ ๋กœ๊ทธ ๋ฐ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง 5. **๋ฐฑ์›Œ๋“œ ํ˜ธํ™˜์„ฑ**: API ์‘๋‹ต ํ˜•์‹ ์œ ์ง€ --- ## ๐ŸŽฏ ์„ฑ๊ณต ๊ธฐ์ค€ ### ๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ - [ ] ๋ชจ๋“  ๊ธฐ์กด API ์—”๋“œํฌ์ธํŠธ ์ •์ƒ ๋™์ž‘ - [ ] ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ - [ ] ํŠธ๋žœ์žญ์…˜ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ - [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋™์ผ์„ฑ ### ์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ - [ ] ์‘๋‹ต ์‹œ๊ฐ„ ๊ธฐ์กด ๋Œ€๋น„ ยฑ10% ์ด๋‚ด - [ ] ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” - [ ] ๋™์‹œ ์ ‘์† ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ ์œ ์ง€ ### ํ’ˆ์งˆ ์š”๊ตฌ์‚ฌํ•ญ - [ ] ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ 90% ์ด์ƒ - [ ] ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ - [ ] ๋ณด์•ˆ ๊ฒ€์ฆ ํ†ต๊ณผ - [ ] ๋ฌธ์„œํ™” ์™„๋ฃŒ ์ด ์ƒ์„ธ ๊ณ„ํš์„ ํ†ตํ•ด ๊ฐ ํŒŒ์ผ๋ณ„๋กœ ์ฒด๊ณ„์ ์ด๊ณ  ์•ˆ์ „ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.