diff --git a/DETAILED_FILE_MIGRATION_PLAN.md b/DETAILED_FILE_MIGRATION_PLAN.md new file mode 100644 index 00000000..56855742 --- /dev/null +++ b/DETAILED_FILE_MIGRATION_PLAN.md @@ -0,0 +1,1216 @@ +# ๐Ÿ“‹ ํŒŒ์ผ๋ณ„ ์ƒ์„ธ 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% ์ด์ƒ +- [ ] ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ +- [ ] ๋ณด์•ˆ ๊ฒ€์ฆ ํ†ต๊ณผ +- [ ] ๋ฌธ์„œํ™” ์™„๋ฃŒ + +์ด ์ƒ์„ธ ๊ณ„ํš์„ ํ†ตํ•ด ๊ฐ ํŒŒ์ผ๋ณ„๋กœ ์ฒด๊ณ„์ ์ด๊ณ  ์•ˆ์ „ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md index dfa30a01..b6b0969c 100644 --- a/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md +++ b/PRISMA_TO_RAW_QUERY_MIGRATION_PLAN.md @@ -8,8 +8,8 @@ ### ๐Ÿ” ํ˜„์žฌ ์ƒํ™ฉ ๋ถ„์„ -- **์ด 42๊ฐœ ํŒŒ์ผ**์—์„œ Prisma ์‚ฌ์šฉ -- **386๊ฐœ์˜ Prisma ํ˜ธ์ถœ** (ORM + Raw Query ํ˜ผ์žฌ) +- **์ด 52๊ฐœ ํŒŒ์ผ**์—์„œ Prisma ์‚ฌ์šฉ +- **490๊ฐœ์˜ Prisma ํ˜ธ์ถœ** (ORM + Raw Query ํ˜ผ์žฌ) - **150๊ฐœ ์ด์ƒ์˜ ํ…Œ์ด๋ธ”** ์ •์˜ (schema.prisma) - **๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ๋ฐ ๋™์  ์ฟผ๋ฆฌ** ๋‹ค์ˆ˜ ์กด์žฌ @@ -17,64 +17,161 @@ ## ๐Ÿ“Š Prisma ์‚ฌ์šฉ ํ˜„ํ™ฉ ๋ถ„์„ +**์ด 42๊ฐœ ํŒŒ์ผ์—์„œ 444๊ฐœ์˜ Prisma ํ˜ธ์ถœ ๋ฐœ๊ฒฌ** โšก (Scripts ์ œ์™ธ) + ### 1. **Prisma ์‚ฌ์šฉ ํŒŒ์ผ ๋ถ„๋ฅ˜** -#### ๐Ÿ”ด **High Priority (ํ•ต์‹ฌ ์„œ๋น„์Šค)** +#### ๐Ÿ”ด **High Priority (ํ•ต์‹ฌ ์„œ๋น„์Šค) - 107๊ฐœ ํ˜ธ์ถœ** ``` backend-node/src/services/ -โ”œโ”€โ”€ authService.ts # ์ธ์ฆ (5๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ dynamicFormService.ts # ๋™์  ํผ (14๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ dataflowControlService.ts # ์ œ์–ด๊ด€๋ฆฌ (6๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ multiConnectionQueryService.ts # ๋‹ค์ค‘ ์—ฐ๊ฒฐ (4๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ tableManagementService.ts # ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ (34๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ screenManagementService.ts # ํ™”๋ฉด ๊ด€๋ฆฌ (40๊ฐœ ํ˜ธ์ถœ) -โ””โ”€โ”€ ddlExecutionService.ts # DDL ์‹คํ–‰ (4๊ฐœ ํ˜ธ์ถœ) -``` - -#### ๐ŸŸก **Medium Priority (๊ด€๋ฆฌ ๊ธฐ๋Šฅ)** - -``` -backend-node/src/services/ -โ”œโ”€โ”€ adminService.ts # ๊ด€๋ฆฌ์ž (3๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ multilangService.ts # ๋‹ค๊ตญ์–ด (22๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ commonCodeService.ts # ๊ณตํ†ต์ฝ”๋“œ (13๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ screenManagementService.ts # ํ™”๋ฉด ๊ด€๋ฆฌ (46๊ฐœ ํ˜ธ์ถœ) โญ ์ตœ์šฐ์„  +โ”œโ”€โ”€ tableManagementService.ts # ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ (35๊ฐœ ํ˜ธ์ถœ) โญ ์ตœ์šฐ์„  +โ”œโ”€โ”€ dataflowService.ts # ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ (31๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ dynamicFormService.ts # ๋™์  ํผ (15๊ฐœ ํ˜ธ์ถœ) โ”œโ”€โ”€ externalDbConnectionService.ts # ์™ธ๋ถ€DB (15๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ batchService.ts # ๋ฐฐ์น˜ (13๊ฐœ ํ˜ธ์ถœ) -โ””โ”€โ”€ eventTriggerService.ts # ์ด๋ฒคํŠธ (6๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ dataflowControlService.ts # ์ œ์–ด๊ด€๋ฆฌ (6๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ ddlExecutionService.ts # DDL ์‹คํ–‰ (6๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ authService.ts # ์ธ์ฆ (5๊ฐœ ํ˜ธ์ถœ) +โ””โ”€โ”€ multiConnectionQueryService.ts # ๋‹ค์ค‘ ์—ฐ๊ฒฐ (4๊ฐœ ํ˜ธ์ถœ) ``` -#### ๐ŸŸข **Low Priority (๋ถ€๊ฐ€ ๊ธฐ๋Šฅ)** +#### ๐ŸŸก **Medium Priority (๊ด€๋ฆฌ ๊ธฐ๋Šฅ) - 142๊ฐœ ํ˜ธ์ถœ** ``` backend-node/src/services/ -โ”œโ”€โ”€ layoutService.ts # ๋ ˆ์ด์•„์›ƒ (8๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ componentStandardService.ts # ์ปดํฌ๋„ŒํŠธ (11๊ฐœ ํ˜ธ์ถœ) -โ”œโ”€โ”€ templateStandardService.ts # ํ…œํ”Œ๋ฆฟ (8๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ multilangService.ts # ๋‹ค๊ตญ์–ด (25๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ batchService.ts # ๋ฐฐ์น˜ (16๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ componentStandardService.ts # ์ปดํฌ๋„ŒํŠธ (16๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ commonCodeService.ts # ๊ณตํ†ต์ฝ”๋“œ (15๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ dataflowDiagramService.ts # ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ (12๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ โ”œโ”€โ”€ collectionService.ts # ์ปฌ๋ ‰์…˜ (11๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ layoutService.ts # ๋ ˆ์ด์•„์›ƒ (10๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ dbTypeCategoryService.ts # DB ํƒ€์ž… ์นดํ…Œ๊ณ ๋ฆฌ (10๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ templateStandardService.ts # ํ…œํ”Œ๋ฆฟ (9๊ฐœ ํ˜ธ์ถœ) +โ”œโ”€โ”€ ddlAuditLogger.ts # DDL ๊ฐ์‚ฌ ๋กœ๊ทธ (8๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ externalCallConfigService.ts # ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • (8๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ batchExternalDbService.ts # ๋ฐฐ์น˜ ์™ธ๋ถ€DB (8๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ 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๊ฐœ ํ˜ธ์ถœ) ``` +#### ๐ŸŸข **Low Priority (์ปจํŠธ๋กค๋Ÿฌ & ๋ผ์šฐํŠธ) - 188๊ฐœ ํ˜ธ์ถœ** + +``` +backend-node/src/controllers/ +โ”œโ”€โ”€ adminController.ts # ๊ด€๋ฆฌ์ž ์ปจํŠธ๋กค๋Ÿฌ (28๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ webTypeStandardController.ts # ์›นํƒ€์ž… ํ‘œ์ค€ (11๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ fileController.ts # ํŒŒ์ผ ์ปจํŠธ๋กค๋Ÿฌ (11๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ buttonActionStandardController.ts # ๋ฒ„ํŠผ ์•ก์…˜ ํ‘œ์ค€ (11๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ entityReferenceController.ts # ์—”ํ‹ฐํ‹ฐ ์ฐธ์กฐ (4๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ”œโ”€โ”€ dataflowExecutionController.ts # ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ (3๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ””โ”€โ”€ screenFileController.ts # ํ™”๋ฉด ํŒŒ์ผ (2๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +backend-node/src/routes/ +โ”œโ”€โ”€ ddlRoutes.ts # DDL ๋ผ์šฐํŠธ (2๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +โ””โ”€โ”€ companyManagementRoutes.ts # ํšŒ์‚ฌ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ (2๊ฐœ ํ˜ธ์ถœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +backend-node/src/config/ +โ””โ”€โ”€ database.ts # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • (4๊ฐœ ํ˜ธ์ถœ) + +#### ๐Ÿ—‘๏ธ **์‚ญ์ œ ์˜ˆ์ • Scripts - 60๊ฐœ ํ˜ธ์ถœ** โš ๏ธ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ + +``` + +backend-node/scripts/ (์‚ญ์ œ ์˜ˆ์ •) +โ”œโ”€โ”€ install-dataflow-indexes.js # ์ธ๋ฑ์Šค ์„ค์น˜ (10๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ add-missing-columns.js # ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (8๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ test-template-creation.js # ํ…œํ”Œ๋ฆฟ ํ…Œ์ŠคํŠธ (6๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ create-component-table.js # ์ปดํฌ๋„ŒํŠธ ํ…Œ์ด๋ธ” ์ƒ์„ฑ (5๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ seed-ui-components.js # UI ์ปดํฌ๋„ŒํŠธ ์‹œ๋“œ (3๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ seed-templates.js # ํ…œํ”Œ๋ฆฟ ์‹œ๋“œ (3๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ init-layout-standards.js # ๋ ˆ์ด์•„์›ƒ ํ‘œ์ค€ ์ดˆ๊ธฐํ™” (3๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ add-data-mapping-column.js # ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (3๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ”œโ”€โ”€ add-button-webtype.js # ๋ฒ„ํŠผ ์›นํƒ€์ž… ์ถ”๊ฐ€ (3๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ +โ””โ”€โ”€ list-components.js # ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (2๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ + +backend-node/ (๋ฃจํŠธ) +โ””โ”€โ”€ clean-screen-tables.js # ํ™”๋ฉด ํ…Œ์ด๋ธ” ์ •๋ฆฌ (7๊ฐœ ํ˜ธ์ถœ) ๐Ÿ—‘๏ธ ์‚ญ์ œ + +```` + +**โš ๏ธ ์‚ญ์ œ ๊ณ„ํš**: ์ด ์Šคํฌ๋ฆฝํŠธ๋“ค์€ ๊ฐœ๋ฐœ/๋ฐฐํฌ ๋„๊ตฌ๋กœ ์šด์˜ ์‹œ์Šคํ…œ์—์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „์— ์‚ญ์ œ ์˜ˆ์ • + ### 2. **๋ณต์žก๋„๋ณ„ ๋ถ„๋ฅ˜** -#### ๐Ÿ”ฅ **๋งค์šฐ ๋ณต์žก (ํŠธ๋žœ์žญ์…˜ + ๋™์  ์ฟผ๋ฆฌ)** +#### ๐Ÿ”ฅ **๋งค์šฐ ๋ณต์žก (ํŠธ๋žœ์žญ์…˜ + ๋™์  ์ฟผ๋ฆฌ) - ์ตœ์šฐ์„  ์ฒ˜๋ฆฌ** -- `dataflowControlService.ts` - ๋ณต์žกํ•œ ์ œ์–ด ๋กœ์ง -- `enhancedDataflowControlService.ts` - ๋‹ค์ค‘ ์—ฐ๊ฒฐ ์ œ์–ด -- `dynamicFormService.ts` - UPSERT ๋ฐ ๋™์  ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ -- `multiConnectionQueryService.ts` - ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ +- `screenManagementService.ts` (46๊ฐœ) - ํ™”๋ฉด ์ •์˜ ๊ด€๋ฆฌ, JSON ์ฒ˜๋ฆฌ +- `tableManagementService.ts` (35๊ฐœ) - ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ, DDL ์‹คํ–‰ +- `dataflowService.ts` (31๊ฐœ) - ๋ณต์žกํ•œ ๊ด€๊ณ„ ๊ด€๋ฆฌ, ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `dynamicFormService.ts` (15๊ฐœ) - UPSERT ๋ฐ ๋™์  ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ +- `externalDbConnectionService.ts` (15๊ฐœ) - ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ๊ด€๋ฆฌ +- `dataflowControlService.ts` (6๊ฐœ) - ๋ณต์žกํ•œ ์ œ์–ด ๋กœ์ง +- `enhancedDataflowControlService.ts` (0๊ฐœ) - ๋‹ค์ค‘ ์—ฐ๊ฒฐ ์ œ์–ด (Raw Query๋งŒ ์‚ฌ์šฉ) +- `multiConnectionQueryService.ts` (4๊ฐœ) - ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ -#### ๐ŸŸ  **๋ณต์žก (Raw Query ํ˜ผ์žฌ)** +#### ๐ŸŸ  **๋ณต์žก (Raw Query ํ˜ผ์žฌ) - 2์ˆœ์œ„** -- `tableManagementService.ts` - ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ -- `screenManagementService.ts` - ํ™”๋ฉด ์ •์˜ ๊ด€๋ฆฌ -- `eventTriggerService.ts` - JSON ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ +- `multilangService.ts` (25๊ฐœ) - ์žฌ๊ท€ ์ฟผ๋ฆฌ, ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ +- `batchService.ts` (16๊ฐœ) - ๋ฐฐ์น˜ ์ž‘์—… ๊ด€๋ฆฌ +- `componentStandardService.ts` (16๊ฐœ) - ์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ๊ด€๋ฆฌ +- `commonCodeService.ts` (15๊ฐœ) - ์ฝ”๋“œ ๊ด€๋ฆฌ, ๊ณ„์ธต ๊ตฌ์กฐ +- `dataflowDiagramService.ts` (12๊ฐœ) - ๋‹ค์ด์–ด๊ทธ๋žจ ๊ด€๋ฆฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `collectionService.ts` (11๊ฐœ) - ์ปฌ๋ ‰์…˜ ๊ด€๋ฆฌ +- `layoutService.ts` (10๊ฐœ) - ๋ ˆ์ด์•„์›ƒ ๊ด€๋ฆฌ +- `dbTypeCategoryService.ts` (10๊ฐœ) - DB ํƒ€์ž… ๋ถ„๋ฅ˜ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `templateStandardService.ts` (9๊ฐœ) - ํ…œํ”Œ๋ฆฟ ํ‘œ์ค€ +- `eventTriggerService.ts` (6๊ฐœ) - JSON ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ -#### ๐ŸŸก **์ค‘๊ฐ„ (๋‹จ์ˆœ CRUD)** +#### ๐ŸŸก **์ค‘๊ฐ„ (๋‹จ์ˆœ CRUD) - 3์ˆœ์œ„** -- `authService.ts` - ์‚ฌ์šฉ์ž ์ธ์ฆ -- `adminService.ts` - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด -- `commonCodeService.ts` - ์ฝ”๋“œ ๊ด€๋ฆฌ +- `ddlAuditLogger.ts` (8๊ฐœ) - DDL ๊ฐ์‚ฌ ๋กœ๊ทธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `externalCallConfigService.ts` (8๊ฐœ) - ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `batchExternalDbService.ts` (8๊ฐœ) - ๋ฐฐ์น˜ ์™ธ๋ถ€DB โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `batchExecutionLogService.ts` (7๊ฐœ) - ๋ฐฐ์น˜ ์‹คํ–‰ ๋กœ๊ทธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `enhancedDynamicFormService.ts` (6๊ฐœ) - ํ™•์žฅ ๋™์  ํผ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `ddlExecutionService.ts` (6๊ฐœ) - DDL ์‹คํ–‰ +- `entityJoinService.ts` (5๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `dataMappingService.ts` (5๊ฐœ) - ๋ฐ์ดํ„ฐ ๋งคํ•‘ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `batchManagementService.ts` (5๊ฐœ) - ๋ฐฐ์น˜ ๊ด€๋ฆฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `authService.ts` (5๊ฐœ) - ์‚ฌ์šฉ์ž ์ธ์ฆ +- `batchSchedulerService.ts` (4๊ฐœ) - ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `dataService.ts` (4๊ฐœ) - ๋ฐ์ดํ„ฐ ์„œ๋น„์Šค โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `adminService.ts` (3๊ฐœ) - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด +- `referenceCacheService.ts` (3๊ฐœ) - ์บ์‹œ ๊ด€๋ฆฌ + +#### ๐ŸŸข **๋‹จ์ˆœ (์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด) - 4์ˆœ์œ„** + +- `adminController.ts` (28๊ฐœ) - ๊ด€๋ฆฌ์ž ์ปจํŠธ๋กค๋Ÿฌ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- `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๊ฐœ) - ํšŒ์‚ฌ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +#### ๐Ÿ—‘๏ธ **์‚ญ์ œ ์˜ˆ์ • Scripts (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ ์•„๋‹˜)** + +- `install-dataflow-indexes.js` (10๊ฐœ) - ์ธ๋ฑ์Šค ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ ๐Ÿ—‘๏ธ +- `add-missing-columns.js` (8๊ฐœ) - ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์Šคํฌ๋ฆฝํŠธ ๐Ÿ—‘๏ธ +- `clean-screen-tables.js` (7๊ฐœ) - ํ…Œ์ด๋ธ” ์ •๋ฆฌ ์Šคํฌ๋ฆฝํŠธ ๐Ÿ—‘๏ธ +- `test-template-creation.js` (6๊ฐœ) - ํ…œํ”Œ๋ฆฟ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ ๐Ÿ—‘๏ธ +- `create-component-table.js` (5๊ฐœ) - ์ปดํฌ๋„ŒํŠธ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๐Ÿ—‘๏ธ +- ๊ธฐํƒ€ ์‹œ๋“œ ์Šคํฌ๋ฆฝํŠธ๋“ค (14๊ฐœ) - ๊ฐœ๋ฐœ์šฉ ๋ฐ์ดํ„ฐ ์‹œ๋“œ ๐Ÿ—‘๏ธ + +**โš ๏ธ ์ค‘์š”**: ์ด ์Šคํฌ๋ฆฝํŠธ๋“ค์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „์— ์‚ญ์ œํ•˜์—ฌ ์ž‘์—…๋Ÿ‰์„ 60๊ฐœ ํ˜ธ์ถœ๋งŒํผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. --- @@ -136,7 +233,7 @@ export class DatabaseManager { await this.pool.end(); } } -``` +```` ### 2. **๋™์  ์ฟผ๋ฆฌ ๋นŒ๋”** @@ -351,77 +448,150 @@ export class DatabaseValidator { - [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ - [ ] ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ ๋„๊ตฌ ์ค€๋น„ -### **Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค ์ „ํ™˜ (2์ฃผ)** +### **Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค ์ „ํ™˜ (3์ฃผ) - ์ตœ์šฐ์„ ** -#### 2.1 ์ธ์ฆ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 1) +#### 2.1 ํ™”๋ฉด ๊ด€๋ฆฌ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 1) - 46๊ฐœ ํ˜ธ์ถœ ```typescript -// ๊ธฐ์กด Prisma ์ฝ”๋“œ -const userInfo = await prisma.user_info.findUnique({ - where: { user_id: userId }, +// ๊ธฐ์กด Prisma ์ฝ”๋“œ (๋ณต์žกํ•œ JSON ์ฒ˜๋ฆฌ) +const screenData = await prisma.screen_definitions.findMany({ + where: { + company_code: companyCode, + screen_config: { path: ["type"], equals: "form" }, + }, + include: { screen_components: true }, }); // ์ƒˆ๋กœ์šด Raw Query ์ฝ”๋“œ -const { query, params } = QueryBuilder.select("user_info", { - where: { user_id: userId }, +const { query, params } = QueryBuilder.select("screen_definitions", { + columns: ["*", "screen_config::jsonb"], + where: { + company_code: companyCode, + "screen_config->>'type'": "form", + }, + joins: [ + { + type: "LEFT", + table: "screen_components", + on: "screen_definitions.id = screen_components.screen_id", + }, + ], }); -const userInfo = await DatabaseManager.query(query, params); +const screenData = await DatabaseManager.query(query, params); ``` -#### 2.2 ๋™์  ํผ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 2) +#### 2.2 ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 2) - 35๊ฐœ ํ˜ธ์ถœ + +- [ ] ๋™์  ํ…Œ์ด๋ธ” ์ƒ์„ฑ/์‚ญ์ œ ๋กœ์ง ์ „ํ™˜ +- [ ] ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ฐœ์„  +- [ ] DDL ์‹คํ–‰ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ +- [ ] ์ปฌ๋Ÿผ ํƒ€์ž… ๋ณ€ํ™˜ ๋กœ์ง ์ตœ์ ํ™” + +#### 2.3 ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 3) - 31๊ฐœ ํ˜ธ์ถœ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +- [ ] ๋ณต์žกํ•œ ๊ด€๊ณ„ ๊ด€๋ฆฌ ๋กœ์ง ์ „ํ™˜ +- [ ] ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์ด๋™ ์ฒ˜๋ฆฌ +- [ ] JSON ๊ธฐ๋ฐ˜ ์„ค์ • ๊ด€๋ฆฌ ๊ฐœ์„  +- [ ] ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์กฐ์ธ ์ตœ์ ํ™” + +#### 2.4 ๋™์  ํผ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 4) - 15๊ฐœ ํ˜ธ์ถœ - [ ] UPSERT ๋กœ์ง Raw Query๋กœ ์ „ํ™˜ - [ ] ๋™์  ํ…Œ์ด๋ธ” ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  - [ ] ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ์ตœ์ ํ™” -#### 2.3 ์ œ์–ด๊ด€๋ฆฌ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 3) +#### 2.5 ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์„œ๋น„์Šค ์ „ํ™˜ (์šฐ์„ ์ˆœ์œ„ 5) - 15๊ฐœ ํ˜ธ์ถœ -- [ ] ๋ณต์žกํ•œ ์กฐ๊ฑด๋ถ€ ์ฟผ๋ฆฌ ์ „ํ™˜ -- [ ] ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ฐœ์„  -- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐ•ํ™” +- [ ] ๋‹ค์ค‘ DB ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ๋กœ์ง +- [ ] ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ +- [ ] ์™ธ๋ถ€ DB ์Šคํ‚ค๋งˆ ๋™๊ธฐํ™” -### **Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ „ํ™˜ (1.5์ฃผ)** +### **Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ์ „ํ™˜ (2.5์ฃผ)** -#### 3.1 ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ ์„œ๋น„์Šค - -- [ ] ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ ์ฟผ๋ฆฌ ์ „ํ™˜ -- [ ] ๋™์  ์ปฌ๋Ÿผ ์ถ”๊ฐ€/์‚ญ์ œ ๋กœ์ง -- [ ] ์ธ๋ฑ์Šค ๊ด€๋ฆฌ ๊ธฐ๋Šฅ - -#### 3.2 ํ™”๋ฉด ๊ด€๋ฆฌ ์„œ๋น„์Šค - -- [ ] JSON ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ตœ์ ํ™” -- [ ] ๋ณต์žกํ•œ ์กฐ์ธ ์ฟผ๋ฆฌ ์ „ํ™˜ -- [ ] ์บ์‹ฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ตฌํ˜„ - -#### 3.3 ๋‹ค๊ตญ์–ด ์„œ๋น„์Šค +#### 3.1 ๋‹ค๊ตญ์–ด ์„œ๋น„์Šค ์ „ํ™˜ - 25๊ฐœ ํ˜ธ์ถœ - [ ] ์žฌ๊ท€ ์ฟผ๋ฆฌ (WITH RECURSIVE) ์ „ํ™˜ - [ ] ๋ฒˆ์—ญ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์ตœ์ ํ™” +- [ ] ๋‹ค๊ตญ์–ด ์บ์‹œ ์‹œ์Šคํ…œ ๊ตฌํ˜„ -### **Phase 4: ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ ์ „ํ™˜ (1์ฃผ)** +#### 3.2 ๋ฐฐ์น˜ ๊ด€๋ จ ์„œ๋น„์Šค ์ „ํ™˜ - 40๊ฐœ ํ˜ธ์ถœ โญ ๋Œ€๊ทœ๋ชจ ์‹ ๊ทœ ๋ฐœ๊ฒฌ -#### 4.1 ๋ฐฐ์น˜ ๋ฐ ์™ธ๋ถ€ ์—ฐ๊ฒฐ +- [ ] `batchService.ts` (16๊ฐœ) - ๋ฐฐ์น˜ ์ž‘์—… ๊ด€๋ฆฌ +- [ ] `batchExternalDbService.ts` (8๊ฐœ) - ๋ฐฐ์น˜ ์™ธ๋ถ€DB +- [ ] `batchExecutionLogService.ts` (7๊ฐœ) - ๋ฐฐ์น˜ ์‹คํ–‰ ๋กœ๊ทธ +- [ ] `batchManagementService.ts` (5๊ฐœ) - ๋ฐฐ์น˜ ๊ด€๋ฆฌ +- [ ] `batchSchedulerService.ts` (4๊ฐœ) - ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ -- [ ] ๋ฐฐ์น˜ ์Šค์ผ€์ค„๋Ÿฌ ์ „ํ™˜ -- [ ] ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ๊ด€๋ฆฌ -- [ ] ๋กœ๊ทธ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง +#### 3.3 ํ‘œ์ค€ ๊ด€๋ฆฌ ์„œ๋น„์Šค ์ „ํ™˜ - 41๊ฐœ ํ˜ธ์ถœ -#### 4.2 ํ‘œ์ค€ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ +- [ ] `componentStandardService.ts` (16๊ฐœ) - ์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ๊ด€๋ฆฌ +- [ ] `commonCodeService.ts` (15๊ฐœ) - ์ฝ”๋“œ ๊ด€๋ฆฌ, ๊ณ„์ธต ๊ตฌ์กฐ +- [ ] `layoutService.ts` (10๊ฐœ) - ๋ ˆ์ด์•„์›ƒ ๊ด€๋ฆฌ -- [ ] ์ปดํฌ๋„ŒํŠธ ํ‘œ์ค€ ๊ด€๋ฆฌ -- [ ] ํ…œํ”Œ๋ฆฟ ํ‘œ์ค€ ๊ด€๋ฆฌ -- [ ] ๋ ˆ์ด์•„์›ƒ ๊ด€๋ฆฌ +#### 3.4 ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๊ด€๋ จ ์„œ๋น„์Šค - 18๊ฐœ ํ˜ธ์ถœ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ -### **Phase 5: Prisma ์™„์ „ ์ œ๊ฑฐ (0.5์ฃผ)** +- [ ] `dataflowDiagramService.ts` (12๊ฐœ) - ๋‹ค์ด์–ด๊ทธ๋žจ ๊ด€๋ฆฌ +- [ ] `dataflowControlService.ts` (6๊ฐœ) - ๋ณต์žกํ•œ ์ œ์–ด ๋กœ์ง -#### 5.1 Prisma ์˜์กด์„ฑ ์ œ๊ฑฐ +#### 3.5 ๊ธฐํƒ€ ์ค‘์š” ์„œ๋น„์Šค - 38๊ฐœ ํ˜ธ์ถœ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +- [ ] `collectionService.ts` (11๊ฐœ) - ์ปฌ๋ ‰์…˜ ๊ด€๋ฆฌ +- [ ] `dbTypeCategoryService.ts` (10๊ฐœ) - DB ํƒ€์ž… ๋ถ„๋ฅ˜ +- [ ] `templateStandardService.ts` (9๊ฐœ) - ํ…œํ”Œ๋ฆฟ ํ‘œ์ค€ +- [ ] `ddlAuditLogger.ts` (8๊ฐœ) - DDL ๊ฐ์‚ฌ ๋กœ๊ทธ + +### **Phase 4: ํ™•์žฅ ๊ธฐ๋Šฅ ์ „ํ™˜ (2.5์ฃผ) โญ ๋Œ€ํญ ํ™•์žฅ** + +#### 4.1 ์™ธ๋ถ€ ์—ฐ๋™ ์„œ๋น„์Šค - 51๊ฐœ ํ˜ธ์ถœ โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +- [ ] `externalCallConfigService.ts` (8๊ฐœ) - ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • +- [ ] `eventTriggerService.ts` (6๊ฐœ) - JSON ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ +- [ ] `enhancedDynamicFormService.ts` (6๊ฐœ) - ํ™•์žฅ ๋™์  ํผ +- [ ] `ddlExecutionService.ts` (6๊ฐœ) - DDL ์‹คํ–‰ +- [ ] `entityJoinService.ts` (5๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ +- [ ] `dataMappingService.ts` (5๊ฐœ) - ๋ฐ์ดํ„ฐ ๋งคํ•‘ +- [ ] `authService.ts` (5๊ฐœ) - ์‚ฌ์šฉ์ž ์ธ์ฆ +- [ ] `multiConnectionQueryService.ts` (4๊ฐœ) - ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ +- [ ] `dataService.ts` (4๊ฐœ) - ๋ฐ์ดํ„ฐ ์„œ๋น„์Šค +- [ ] `adminService.ts` (3๊ฐœ) - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด +- [ ] `referenceCacheService.ts` (3๊ฐœ) - ์บ์‹œ ๊ด€๋ฆฌ + +#### 4.2 ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด ์ „ํ™˜ - 72๊ฐœ ํ˜ธ์ถœ โญ ๋Œ€๊ทœ๋ชจ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + +- [ ] `adminController.ts` (28๊ฐœ) - ๊ด€๋ฆฌ์ž ์ปจํŠธ๋กค๋Ÿฌ +- [ ] `webTypeStandardController.ts` (11๊ฐœ) - ์›นํƒ€์ž… ํ‘œ์ค€ +- [ ] `fileController.ts` (11๊ฐœ) - ํŒŒ์ผ ์ปจํŠธ๋กค๋Ÿฌ +- [ ] `buttonActionStandardController.ts` (11๊ฐœ) - ๋ฒ„ํŠผ ์•ก์…˜ ํ‘œ์ค€ +- [ ] `entityReferenceController.ts` (4๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์ฐธ์กฐ +- [ ] `dataflowExecutionController.ts` (3๊ฐœ) - ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹คํ–‰ +- [ ] `screenFileController.ts` (2๊ฐœ) - ํ™”๋ฉด ํŒŒ์ผ +- [ ] `ddlRoutes.ts` (2๊ฐœ) - DDL ๋ผ์šฐํŠธ + +#### 4.3 ์„ค์ • ๋ฐ ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ - 6๊ฐœ ํ˜ธ์ถœ + +- [ ] `database.ts` (4๊ฐœ) - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • +- [ ] `companyManagementRoutes.ts` (2๊ฐœ) - ํšŒ์‚ฌ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ + +### **Phase 5: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Scripts ์‚ญ์ œ (0.5์ฃผ) ๐Ÿ—‘๏ธ** + +#### 5.1 ๋ถˆํ•„์š”ํ•œ ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ์‚ญ์ œ - 60๊ฐœ ํ˜ธ์ถœ ์ œ๊ฑฐ + +- [ ] `backend-node/scripts/` ์ „์ฒด ํด๋” ์‚ญ์ œ (53๊ฐœ ํ˜ธ์ถœ) +- [ ] `backend-node/clean-screen-tables.js` ์‚ญ์ œ (7๊ฐœ ํ˜ธ์ถœ) +- [ ] ๊ด€๋ จ package.json ์Šคํฌ๋ฆฝํŠธ ์ •๋ฆฌ +- [ ] ๋ฌธ์„œ์—์„œ ์Šคํฌ๋ฆฝํŠธ ์ฐธ์กฐ ์ œ๊ฑฐ + +**โœ… ํšจ๊ณผ**: 60๊ฐœ Prisma ํ˜ธ์ถœ์„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์—†์ด ์ œ๊ฑฐํ•˜์—ฌ ์ž‘์—…๋Ÿ‰ ๋Œ€ํญ ๊ฐ์†Œ + +### **Phase 6: Prisma ์™„์ „ ์ œ๊ฑฐ (0.5์ฃผ)** + +#### 6.1 Prisma ์˜์กด์„ฑ ์ œ๊ฑฐ - [ ] `package.json`์—์„œ Prisma ์ œ๊ฑฐ - [ ] `schema.prisma` ํŒŒ์ผ ์‚ญ์ œ - [ ] ๊ด€๋ จ ์„ค์ • ํŒŒ์ผ ์ •๋ฆฌ -#### 5.2 ์ตœ์ข… ๊ฒ€์ฆ ๋ฐ ์ตœ์ ํ™” +#### 6.2 ์ตœ์ข… ๊ฒ€์ฆ ๋ฐ ์ตœ์ ํ™” - [ ] ์ „์ฒด ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ - [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” @@ -858,49 +1028,73 @@ describe("Performance Benchmarks", () => { ## ๐Ÿ“‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ -### **Phase 1: ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ (1์ฃผ)** +### **Phase 1: ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ (1์ฃผ)** โœ… **์™„๋ฃŒ** -- [ ] DatabaseManager ํด๋ž˜์Šค ๊ตฌํ˜„ -- [ ] QueryBuilder ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ตฌํ˜„ -- [ ] ํƒ€์ž… ์ •์˜ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง -- [ ] ์—ฐ๊ฒฐ ํ’€ ์„ค์ • ๋ฐ ์ตœ์ ํ™” -- [ ] ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ -- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฉ”์ปค๋‹ˆ์ฆ˜ -- [ ] ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ -- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ +- [x] DatabaseManager ํด๋ž˜์Šค ๊ตฌํ˜„ (`backend-node/src/database/db.ts`) +- [x] QueryBuilder ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ตฌํ˜„ (`backend-node/src/utils/queryBuilder.ts`) +- [x] ํƒ€์ž… ์ •์˜ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง (`backend-node/src/types/database.ts`) +- [x] ์—ฐ๊ฒฐ ํ’€ ์„ค์ • ๋ฐ ์ตœ์ ํ™” (pg Pool ์‚ฌ์šฉ) +- [x] ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ (transaction ํ•จ์ˆ˜ ๊ตฌํ˜„) +- [x] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฉ”์ปค๋‹ˆ์ฆ˜ (try-catch ๋ฐ rollback ์ฒ˜๋ฆฌ) +- [x] ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ (์ฟผ๋ฆฌ ๋กœ๊ทธ ํฌํ•จ) +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (`backend-node/src/tests/`) +- [x] ํ…Œ์ŠคํŠธ ์„ฑ๊ณต ํ™•์ธ (multiConnectionQueryService, externalCallConfigService) -### **Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค (2์ฃผ)** +### **Phase 2: ํ•ต์‹ฌ ์„œ๋น„์Šค (3์ฃผ) - 107๊ฐœ ํ˜ธ์ถœ** -- [ ] AuthService ์ „ํ™˜ ๋ฐ ํ…Œ์ŠคํŠธ -- [ ] DynamicFormService ์ „ํ™˜ (UPSERT ํฌํ•จ) -- [ ] DataflowControlService ์ „ํ™˜ (๋ณต์žกํ•œ ๋กœ์ง) -- [ ] MultiConnectionQueryService ์ „ํ™˜ -- [ ] TableManagementService ์ „ํ™˜ -- [ ] ScreenManagementService ์ „ํ™˜ -- [ ] DDLExecutionService ์ „ํ™˜ +- [ ] ScreenManagementService ์ „ํ™˜ (46๊ฐœ) - ์ตœ์šฐ์„  +- [ ] TableManagementService ์ „ํ™˜ (35๊ฐœ) - ์ตœ์šฐ์„  +- [ ] DataflowService ์ „ํ™˜ (31๊ฐœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ +- [ ] DynamicFormService ์ „ํ™˜ (15๊ฐœ) - UPSERT ํฌํ•จ +- [ ] ExternalDbConnectionService ์ „ํ™˜ (15๊ฐœ) +- [ ] DataflowControlService ์ „ํ™˜ (6๊ฐœ) - ๋ณต์žกํ•œ ๋กœ์ง +- [ ] DDLExecutionService ์ „ํ™˜ (6๊ฐœ) +- [ ] AuthService ์ „ํ™˜ (5๊ฐœ) +- [ ] MultiConnectionQueryService ์ „ํ™˜ (4๊ฐœ) - [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰ -### **Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ (1.5์ฃผ)** +### **Phase 3: ๊ด€๋ฆฌ ๊ธฐ๋Šฅ (2.5์ฃผ) - 162๊ฐœ ํ˜ธ์ถœ** -- [ ] AdminService ์ „ํ™˜ -- [ ] MultiLangService ์ „ํ™˜ (์žฌ๊ท€ ์ฟผ๋ฆฌ) -- [ ] CommonCodeService ์ „ํ™˜ -- [ ] ExternalDbConnectionService ์ „ํ™˜ -- [ ] BatchService ๋ฐ ๊ด€๋ จ ์„œ๋น„์Šค ์ „ํ™˜ -- [ ] EventTriggerService ์ „ํ™˜ +- [ ] MultiLangService ์ „ํ™˜ (25๊ฐœ) - ์žฌ๊ท€ ์ฟผ๋ฆฌ +- [ ] ๋ฐฐ์น˜ ๊ด€๋ จ ์„œ๋น„์Šค ์ „ํ™˜ (40๊ฐœ) โญ ๋Œ€๊ทœ๋ชจ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + - [ ] BatchService (16๊ฐœ), BatchExternalDbService (8๊ฐœ) + - [ ] BatchExecutionLogService (7๊ฐœ), BatchManagementService (5๊ฐœ) + - [ ] BatchSchedulerService (4๊ฐœ) +- [ ] ํ‘œ์ค€ ๊ด€๋ฆฌ ์„œ๋น„์Šค ์ „ํ™˜ (41๊ฐœ) + - [ ] ComponentStandardService (16๊ฐœ), CommonCodeService (15๊ฐœ) + - [ ] LayoutService (10๊ฐœ) +- [ ] ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๊ด€๋ จ ์„œ๋น„์Šค (18๊ฐœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + - [ ] DataflowDiagramService (12๊ฐœ), DataflowControlService (6๊ฐœ) +- [ ] ๊ธฐํƒ€ ์ค‘์š” ์„œ๋น„์Šค (38๊ฐœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + - [ ] CollectionService (11๊ฐœ), DbTypeCategoryService (10๊ฐœ) + - [ ] TemplateStandardService (9๊ฐœ), DDLAuditLogger (8๊ฐœ) - [ ] ๊ธฐ๋Šฅ๋ณ„ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ -### **Phase 4: ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ (1์ฃผ)** +### **Phase 4: ํ™•์žฅ ๊ธฐ๋Šฅ (2.5์ฃผ) - 129๊ฐœ ํ˜ธ์ถœ โญ ๋Œ€ํญ ํ™•์žฅ** -- [ ] LayoutService ์ „ํ™˜ -- [ ] ComponentStandardService ์ „ํ™˜ -- [ ] TemplateStandardService ์ „ํ™˜ -- [ ] CollectionService ์ „ํ™˜ -- [ ] ReferenceCacheService ์ „ํ™˜ -- [ ] ๊ธฐํƒ€ ์ปจํŠธ๋กค๋Ÿฌ ์ „ํ™˜ +- [ ] ์™ธ๋ถ€ ์—ฐ๋™ ์„œ๋น„์Šค ์ „ํ™˜ (51๊ฐœ) โญ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + - [ ] ExternalCallConfigService (8๊ฐœ), EventTriggerService (6๊ฐœ) + - [ ] EnhancedDynamicFormService (6๊ฐœ), EntityJoinService (5๊ฐœ) + - [ ] DataMappingService (5๊ฐœ), DataService (4๊ฐœ) + - [ ] AdminService (3๊ฐœ), ReferenceCacheService (3๊ฐœ) +- [ ] ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด ์ „ํ™˜ (72๊ฐœ) โญ ๋Œ€๊ทœ๋ชจ ์‹ ๊ทœ ๋ฐœ๊ฒฌ + - [ ] AdminController (28๊ฐœ), WebTypeStandardController (11๊ฐœ) + - [ ] FileController (11๊ฐœ), ButtonActionStandardController (11๊ฐœ) + - [ ] EntityReferenceController (4๊ฐœ), DataflowExecutionController (3๊ฐœ) + - [ ] ScreenFileController (2๊ฐœ), DDLRoutes (2๊ฐœ) +- [ ] ์„ค์ • ๋ฐ ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ (6๊ฐœ) + - [ ] Database.ts (4๊ฐœ), CompanyManagementRoutes (2๊ฐœ) - [ ] ์ „์ฒด ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ -### **Phase 5: ์™„์ „ ์ œ๊ฑฐ (0.5์ฃผ)** +### **Phase 5: Scripts ์‚ญ์ œ (0.5์ฃผ) - 60๊ฐœ ํ˜ธ์ถœ ์ œ๊ฑฐ ๐Ÿ—‘๏ธ** + +- [ ] ๋ถˆํ•„์š”ํ•œ ์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ ์‚ญ์ œ (60๊ฐœ) ๐Ÿ—‘๏ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š” + - [ ] backend-node/scripts/ ์ „์ฒด ํด๋” ์‚ญ์ œ (53๊ฐœ) + - [ ] backend-node/clean-screen-tables.js ์‚ญ์ œ (7๊ฐœ) + - [ ] package.json ์Šคํฌ๋ฆฝํŠธ ์ •๋ฆฌ +- [ ] ๋ฌธ์„œ์—์„œ ์Šคํฌ๋ฆฝํŠธ ์ฐธ์กฐ ์ œ๊ฑฐ + +### **Phase 6: ์™„์ „ ์ œ๊ฑฐ (0.5์ฃผ)** - [ ] Prisma ์˜์กด์„ฑ ์ œ๊ฑฐ - [ ] schema.prisma ์‚ญ์ œ @@ -962,8 +1156,70 @@ describe("Performance Benchmarks", () => { --- -**์ด ์˜ˆ์ƒ ๊ธฐ๊ฐ„: 6์ฃผ** -**ํ•ต์‹ฌ ๊ฐœ๋ฐœ์ž: 2-3๋ช…** -**์œ„ํ—˜๋„: ์ค‘๊ฐ„ (์ ์ ˆํ•œ ๊ณ„ํš๊ณผ ํ…Œ์ŠคํŠธ๋กœ ๊ด€๋ฆฌ ๊ฐ€๋Šฅ)** +--- -์ด ๊ณ„ํš์„ ํ†ตํ•ด Prisma๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ์ง„์ •ํ•œ ๋™์  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿš€ +## ๐Ÿ“ˆ **์—…๋ฐ์ดํŠธ๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ทœ๋ชจ** + +### **๐Ÿ” ์ตœ์ข… Prisma ์‚ฌ์šฉ ํ˜„ํ™ฉ (Scripts ์‚ญ์ œ ํ›„)** + +- **๊ธฐ์กด ๊ณ„ํš**: 42๊ฐœ ํŒŒ์ผ, 386๊ฐœ ํ˜ธ์ถœ +- **Scripts ํฌํ•จ**: 52๊ฐœ ํŒŒ์ผ, 490๊ฐœ ํ˜ธ์ถœ (+104๊ฐœ ํ˜ธ์ถœ ๋ฐœ๊ฒฌ) +- **Scripts ์‚ญ์ œ ํ›„**: **42๊ฐœ ํŒŒ์ผ, 444๊ฐœ ํ˜ธ์ถœ** (+58๊ฐœ ํ˜ธ์ถœ ์‹ค์ œ ์ฆ๊ฐ€) โšก + +### **โญ ์ฃผ์š” ์‹ ๊ทœ ๋ฐœ๊ฒฌ ์„œ๋น„์Šค๋“ค** + +1. **`dataflowService.ts`** (31๊ฐœ) - ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ ํ•ต์‹ฌ ์„œ๋น„์Šค +2. **๋ฐฐ์น˜ ๊ด€๋ จ ์„œ๋น„์Šค๋“ค** (40๊ฐœ) - 5๊ฐœ ์„œ๋น„์Šค๋กœ ๋ถ„์‚ฐ๋œ ๋Œ€๊ทœ๋ชจ ๋ฐฐ์น˜ ์‹œ์Šคํ…œ +3. **`dataflowDiagramService.ts`** (12๊ฐœ) - ๋‹ค์ด์–ด๊ทธ๋žจ ๊ด€๋ฆฌ +4. **`dbTypeCategoryService.ts`** (10๊ฐœ) - DB ํƒ€์ž… ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ +5. **์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด** (72๊ฐœ) - 7๊ฐœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋Œ€๊ทœ๋ชจ Prisma ์‚ฌ์šฉ +6. **๊ฐ์‚ฌ ๋ฐ ๋กœ๊น… ์„œ๋น„์Šค๋“ค** (15๊ฐœ) - DDL ๊ฐ์‚ฌ, ๋ฐฐ์น˜ ์‹คํ–‰ ๋กœ๊ทธ +7. **ํ™•์žฅ ๊ธฐ๋Šฅ๋“ค** (26๊ฐœ) - ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ, ๋ฐ์ดํ„ฐ ๋งคํ•‘, ์™ธ๋ถ€ ํ˜ธ์ถœ ์„ค์ • +8. **๐Ÿ—‘๏ธ Scripts ์‚ญ์ œ** (60๊ฐœ) - ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฐœ๋ฐœ/๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š”) + +### **๐Ÿ“Š ์šฐ์„ ์ˆœ์œ„ ์žฌ์กฐ์ •** + +#### **๐Ÿ”ด ์ตœ์šฐ์„  (Phase 2) - 107๊ฐœ ํ˜ธ์ถœ** + +- ํ™”๋ฉด๊ด€๋ฆฌ (46๊ฐœ), ํ…Œ์ด๋ธ”๊ด€๋ฆฌ (35๊ฐœ), ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ (31๊ฐœ) + +#### **๐ŸŸก ๊ณ ์šฐ์„ ์ˆœ์œ„ (Phase 3) - 162๊ฐœ ํ˜ธ์ถœ** + +- ๋‹ค๊ตญ์–ด (25๊ฐœ), ๋ฐฐ์น˜ ์‹œ์Šคํ…œ (40๊ฐœ), ํ‘œ์ค€ ๊ด€๋ฆฌ (41๊ฐœ) + +#### **๐ŸŸข ์ค‘๊ฐ„์šฐ์„ ์ˆœ์œ„ (Phase 4) - 129๊ฐœ ํ˜ธ์ถœ** + +- ์™ธ๋ถ€ ์—ฐ๋™ (51๊ฐœ), ์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด (72๊ฐœ), ๊ธฐํƒ€ (6๊ฐœ) + +#### **๐Ÿ—‘๏ธ Scripts ์‚ญ์ œ (Phase 5) - 60๊ฐœ ํ˜ธ์ถœ** ๐Ÿ—‘๏ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š” + +- ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฐœ๋ฐœ/๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ (60๊ฐœ) - ์‚ญ์ œ๋กœ ์ž‘์—…๋Ÿ‰ ๊ฐ์†Œ + +--- + +## ๐ŸŽฏ **์ตœ์ข… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš** + +**์ด ์˜ˆ์ƒ ๊ธฐ๊ฐ„: 8์ฃผ** โฌ†๏ธ (+2์ฃผ ์—ฐ์žฅ, Scripts ์‚ญ์ œ๋กœ 1์ฃผ ๋‹จ์ถ•) +**ํ•ต์‹ฌ ๊ฐœ๋ฐœ์ž: 3-4๋ช…** โฌ†๏ธ (+1๋ช… ์ถ”๊ฐ€) +**์‹ค์ œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ: 444๊ฐœ ํ˜ธ์ถœ** (Scripts 60๊ฐœ ์ œ์™ธ) +**์œ„ํ—˜๋„: ์ค‘๊ฐ„-๋†’์Œ** โฌ‡๏ธ (Scripts ์‚ญ์ œ๋กœ ์œ„ํ—˜๋„ ์ผ๋ถ€ ๊ฐ์†Œ) + +### **โš ๏ธ ์ฃผ์š” ์œ„ํ—˜ ์š”์†Œ** + +1. **๋ฐฐ์น˜ ์‹œ์Šคํ…œ ๋ณต์žก์„ฑ**: 5๊ฐœ ์„œ๋น„์Šค 40๊ฐœ ํ˜ธ์ถœ์˜ ๋ณต์žกํ•œ ์˜์กด์„ฑ +2. **์ปจํŠธ๋กค๋Ÿฌ ๋ ˆ์ด์–ด ๊ทœ๋ชจ**: 72๊ฐœ ํ˜ธ์ถœ์˜ ๋Œ€๊ทœ๋ชจ API ์ „ํ™˜ +3. **๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ์‹œ์Šคํ…œ**: ์‹ ๊ทœ ๋ฐœ๊ฒฌ๋œ ํ•ต์‹ฌ ์„œ๋น„์Šค (31๊ฐœ ํ˜ธ์ถœ) +4. **ํŠธ๋žœ์žญ์…˜ ๋ณต์žก์„ฑ**: ๋‹ค์ค‘ ์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ๋ณด์žฅ +5. **โœ… Scripts ์‚ญ์ œ**: 60๊ฐœ ํ˜ธ์ถœ ์ œ๊ฑฐ๋กœ ์ž‘์—…๋Ÿ‰ ๋Œ€ํญ ๊ฐ์†Œ + +### **๐Ÿš€ ์„ฑ๊ณต์„ ์œ„ํ•œ ํ•ต์‹ฌ ์ „๋žต** + +1. **๋‹จ๊ณ„๋ณ„ ์ ์ง„์  ์ „ํ™˜**: ์ ˆ๋Œ€ ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๊ฒƒ์„ ๋ฐ”๊พธ์ง€ ์•Š๊ธฐ +2. **์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ**: ๊ฐ Phase๋งˆ๋‹ค ์™„์ „ํ•œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ +3. **๋กค๋ฐฑ ๊ณ„ํš**: ๊ฐ ๋‹จ๊ณ„๋ณ„ ์ฆ‰์‹œ ๋กค๋ฐฑ ๊ฐ€๋Šฅํ•œ ๊ณ„ํš ์ˆ˜๋ฆฝ +4. **๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ•ํ™”**: ์ „ํ™˜ ํ›„ ์„ฑ๋Šฅ ๋ฐ ์•ˆ์ •์„ฑ ์ง€์† ๋ชจ๋‹ˆํ„ฐ๋ง +5. **ํŒ€ ํ™•๋Œ€**: ๋ณต์žก์„ฑ ์ฆ๊ฐ€๋กœ ์ธํ•œ ๊ฐœ๋ฐœํŒ€ ํ™•๋Œ€ ํ•„์š” + +์ด **์™„์ „ํ•œ ๋ถ„์„**์„ ํ†ตํ•ด Prisma๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ์ง„์ •ํ•œ ๋™์  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿš€ + +**โšก ์ค‘์š”**: ์ด์ œ ๋ชจ๋“  Prisma ์‚ฌ์šฉ ๋ถ€๋ถ„์ด ํŒŒ์•…๋˜์—ˆ์œผ๋ฏ€๋กœ, ๋ˆ„๋ฝ ์—†๋Š” ์™„์ „ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. diff --git a/backend-node/PHASE1_USAGE_GUIDE.md b/backend-node/PHASE1_USAGE_GUIDE.md new file mode 100644 index 00000000..a3d4b8ba --- /dev/null +++ b/backend-node/PHASE1_USAGE_GUIDE.md @@ -0,0 +1,418 @@ +# Phase 1: Raw Query ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ + +## ๐Ÿ“‹ ๊ฐœ์š” + +Phase 1์—์„œ ๊ตฌํ˜„ํ•œ Raw Query ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•„ํ‚คํ…์ฒ˜ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. + +--- + +## ๐Ÿ—๏ธ ๊ตฌํ˜„๋œ ๋ชจ๋“ˆ + +### 1. **DatabaseManager** (`src/database/db.ts`) + +PostgreSQL ์—ฐ๊ฒฐ ํ’€ ๊ธฐ๋ฐ˜ ํ•ต์‹ฌ ๋ชจ๋“ˆ + +**์ฃผ์š” ํ•จ์ˆ˜:** +- `query(sql, params)` - ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ์‹คํ–‰ +- `queryOne(sql, params)` - ๋‹จ์ผ ํ–‰ ์กฐํšŒ +- `transaction(callback)` - ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ +- `getPool()` - ์—ฐ๊ฒฐ ํ’€ ๊ฐ€์ ธ์˜ค๊ธฐ +- `getPoolStatus()` - ์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ ํ™•์ธ + +### 2. **QueryBuilder** (`src/utils/queryBuilder.ts`) + +๋™์  ์ฟผ๋ฆฌ ์ƒ์„ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ + +**์ฃผ์š” ๋ฉ”์„œ๋“œ:** +- `QueryBuilder.select(tableName, options)` - SELECT ์ฟผ๋ฆฌ +- `QueryBuilder.insert(tableName, data, options)` - INSERT ์ฟผ๋ฆฌ +- `QueryBuilder.update(tableName, data, where, options)` - UPDATE ์ฟผ๋ฆฌ +- `QueryBuilder.delete(tableName, where, options)` - DELETE ์ฟผ๋ฆฌ +- `QueryBuilder.count(tableName, where)` - COUNT ์ฟผ๋ฆฌ +- `QueryBuilder.exists(tableName, where)` - EXISTS ์ฟผ๋ฆฌ + +### 3. **DatabaseValidator** (`src/utils/databaseValidator.ts`) + +SQL Injection ๋ฐฉ์ง€ ๋ฐ ์ž…๋ ฅ ๊ฒ€์ฆ + +**์ฃผ์š” ๋ฉ”์„œ๋“œ:** +- `validateTableName(tableName)` - ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ +- `validateColumnName(columnName)` - ์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ +- `validateWhereClause(where)` - WHERE ์กฐ๊ฑด ๊ฒ€์ฆ +- `sanitizeInput(input)` - ์ž…๋ ฅ ๊ฐ’ Sanitize + +### 4. **ํƒ€์ž… ์ •์˜** (`src/types/database.ts`) + +TypeScript ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ + +--- + +## ๐Ÿš€ ์‚ฌ์šฉ ์˜ˆ์ œ + +### 1. ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ์‹คํ–‰ + +```typescript +import { query, queryOne } from '../database/db'; + +// ์—ฌ๋Ÿฌ ํ–‰ ์กฐํšŒ +const users = await query( + 'SELECT * FROM users WHERE status = $1', + ['active'] +); + +// ๋‹จ์ผ ํ–‰ ์กฐํšŒ +const user = await queryOne( + 'SELECT * FROM users WHERE user_id = $1', + ['user123'] +); + +if (!user) { + throw new Error('์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); +} +``` + +### 2. QueryBuilder ์‚ฌ์šฉ + +#### SELECT + +```typescript +import { query } from '../database/db'; +import { QueryBuilder } from '../utils/queryBuilder'; + +// ๊ธฐ๋ณธ SELECT +const { query: sql, params } = QueryBuilder.select('users', { + where: { status: 'active' }, + orderBy: 'created_at DESC', + limit: 10, +}); + +const users = await query(sql, params); + +// ๋ณต์žกํ•œ SELECT (JOIN, WHERE, ORDER BY) +const { query: sql2, params: params2 } = QueryBuilder.select('users', { + columns: ['users.user_id', 'users.username', 'departments.dept_name'], + joins: [ + { + type: 'LEFT', + table: 'departments', + on: 'users.dept_id = departments.dept_id', + }, + ], + where: { 'users.status': 'active' }, + orderBy: ['users.created_at DESC', 'users.username ASC'], + limit: 20, + offset: 0, +}); + +const result = await query(sql2, params2); +``` + +#### INSERT + +```typescript +import { query } from '../database/db'; +import { QueryBuilder } from '../utils/queryBuilder'; + +// ๊ธฐ๋ณธ INSERT +const { query: sql, params } = QueryBuilder.insert( + 'users', + { + user_id: 'new_user', + username: 'John Doe', + email: 'john@example.com', + status: 'active', + }, + { + returning: ['id', 'user_id'], + } +); + +const [newUser] = await query(sql, params); +console.log('์ƒ์„ฑ๋œ ์‚ฌ์šฉ์ž ID:', newUser.id); + +// UPSERT (INSERT ... ON CONFLICT) +const { query: sql2, params: params2 } = QueryBuilder.insert( + 'users', + { + user_id: 'user123', + username: 'Jane', + email: 'jane@example.com', + }, + { + onConflict: { + columns: ['user_id'], + action: 'DO UPDATE', + updateSet: ['username', 'email'], + }, + returning: ['*'], + } +); + +const [upsertedUser] = await query(sql2, params2); +``` + +#### UPDATE + +```typescript +import { query } from '../database/db'; +import { QueryBuilder } from '../utils/queryBuilder'; + +const { query: sql, params } = QueryBuilder.update( + 'users', + { + username: 'Updated Name', + email: 'updated@example.com', + updated_at: new Date(), + }, + { + user_id: 'user123', + }, + { + returning: ['*'], + } +); + +const [updatedUser] = await query(sql, params); +``` + +#### DELETE + +```typescript +import { query } from '../database/db'; +import { QueryBuilder } from '../utils/queryBuilder'; + +const { query: sql, params } = QueryBuilder.delete( + 'users', + { + user_id: 'user_to_delete', + }, + { + returning: ['user_id', 'username'], + } +); + +const [deletedUser] = await query(sql, params); +console.log('์‚ญ์ œ๋œ ์‚ฌ์šฉ์ž:', deletedUser.username); +``` + +### 3. ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ + +```typescript +import { transaction } from '../database/db'; + +// ๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ +const result = await transaction(async (client) => { + // 1. ์‚ฌ์šฉ์ž ์ƒ์„ฑ + const userResult = await client.query( + 'INSERT INTO users (user_id, username, email) VALUES ($1, $2, $3) RETURNING id', + ['new_user', 'John', 'john@example.com'] + ); + + const userId = userResult.rows[0].id; + + // 2. ์—ญํ•  ํ• ๋‹น + await client.query( + 'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)', + [userId, 'admin'] + ); + + // 3. ๋กœ๊ทธ ์ƒ์„ฑ + await client.query( + 'INSERT INTO audit_logs (action, user_id, details) VALUES ($1, $2, $3)', + ['USER_CREATED', userId, JSON.stringify({ username: 'John' })] + ); + + return { success: true, userId }; +}); + +console.log('ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ:', result); +``` + +### 4. JSON ํ•„๋“œ ์ฟผ๋ฆฌ (JSONB) + +```typescript +import { query } from '../database/db'; +import { QueryBuilder } from '../utils/queryBuilder'; + +// JSON ํ•„๋“œ ์ฟผ๋ฆฌ (config->>'type' = 'form') +const { query: sql, params } = QueryBuilder.select('screen_management', { + columns: ['*'], + where: { + company_code: 'COMPANY_001', + "config->>'type'": 'form', + }, +}); + +const screens = await query(sql, params); +``` + +### 5. ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ + +```typescript +import { query } from '../database/db'; +import { DatabaseValidator } from '../utils/databaseValidator'; + +async function queryDynamicTable(tableName: string, filters: Record) { + // ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ (SQL Injection ๋ฐฉ์ง€) + if (!DatabaseValidator.validateTableName(tableName)) { + throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”๋ช…์ž…๋‹ˆ๋‹ค.'); + } + + // WHERE ์กฐ๊ฑด ๊ฒ€์ฆ + if (!DatabaseValidator.validateWhereClause(filters)) { + throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ WHERE ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค.'); + } + + const { query: sql, params } = QueryBuilder.select(tableName, { + where: filters, + }); + + return await query(sql, params); +} + +// ์‚ฌ์šฉ ์˜ˆ +const data = await queryDynamicTable('company_data_001', { + status: 'active', + region: 'Seoul', +}); +``` + +--- + +## ๐Ÿ” ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### 1. **ํ•ญ์ƒ Parameterized Query ์‚ฌ์šฉ** + +```typescript +// โŒ ์œ„ํ—˜: SQL Injection ์ทจ์•ฝ +const userId = req.params.userId; +const sql = `SELECT * FROM users WHERE user_id = '${userId}'`; +const users = await query(sql); + +// โœ… ์•ˆ์ „: Parameterized Query +const userId = req.params.userId; +const users = await query('SELECT * FROM users WHERE user_id = $1', [userId]); +``` + +### 2. **์‹๋ณ„์ž ๊ฒ€์ฆ** + +```typescript +import { DatabaseValidator } from '../utils/databaseValidator'; + +// ํ…Œ์ด๋ธ”๋ช…/์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ +if (!DatabaseValidator.validateTableName(tableName)) { + throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”๋ช…์ž…๋‹ˆ๋‹ค.'); +} + +if (!DatabaseValidator.validateColumnName(columnName)) { + throw new Error('์œ ํšจํ•˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ๋ช…์ž…๋‹ˆ๋‹ค.'); +} +``` + +### 3. **์ž…๋ ฅ ๊ฐ’ Sanitize** + +```typescript +import { DatabaseValidator } from '../utils/databaseValidator'; + +const sanitizedData = DatabaseValidator.sanitizeInput(userInput); +``` + +--- + +## ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ + +### 1. **์—ฐ๊ฒฐ ํ’€ ๋ชจ๋‹ˆํ„ฐ๋ง** + +```typescript +import { getPoolStatus } from '../database/db'; + +const status = getPoolStatus(); +console.log('์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ:', { + total: status.totalCount, + idle: status.idleCount, + waiting: status.waitingCount, +}); +``` + +### 2. **๋ฐฐ์น˜ INSERT** + +```typescript +import { transaction } from '../database/db'; + +// ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… ์‹œ ํŠธ๋žœ์žญ์…˜ ์‚ฌ์šฉ +await transaction(async (client) => { + for (const item of largeDataset) { + await client.query('INSERT INTO items (name, value) VALUES ($1, $2)', [ + item.name, + item.value, + ]); + } +}); +``` + +### 3. **์ธ๋ฑ์Šค ํ™œ์šฉ ์ฟผ๋ฆฌ** + +```typescript +// WHERE ์ ˆ์— ์ธ๋ฑ์Šค ์ปฌ๋Ÿผ ์‚ฌ์šฉ +const { query: sql, params } = QueryBuilder.select('users', { + where: { + user_id: 'user123', // ์ธ๋ฑ์Šค ์ปฌ๋Ÿผ + }, +}); +``` + +--- + +## ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹คํ–‰ + +```bash +# ํ…Œ์ŠคํŠธ ์‹คํ–‰ +npm test -- database.test.ts + +# ํŠน์ • ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰ +npm test -- database.test.ts -t "QueryBuilder" +``` + +--- + +## ๐Ÿšจ ์—๋Ÿฌ ํ•ธ๋“ค๋ง + +```typescript +import { query } from '../database/db'; + +try { + const users = await query('SELECT * FROM users WHERE status = $1', ['active']); + return users; +} catch (error: any) { + console.error('์ฟผ๋ฆฌ ์‹คํ–‰ ์‹คํŒจ:', error.message); + + // PostgreSQL ์—๋Ÿฌ ์ฝ”๋“œ ํ™•์ธ + if (error.code === '23505') { + throw new Error('์ค‘๋ณต๋œ ๊ฐ’์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.'); + } + + if (error.code === '23503') { + throw new Error('์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค.'); + } + + throw error; +} +``` + +--- + +## ๐Ÿ“ ๋‹ค์Œ ๋‹จ๊ณ„ (Phase 2) + +Phase 1 ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๊ฐ€ ์™„์„ฑ๋˜์—ˆ์œผ๋ฏ€๋กœ, Phase 2์—์„œ๋Š”: + +1. **screenManagementService.ts** ์ „ํ™˜ (46๊ฐœ ํ˜ธ์ถœ) +2. **tableManagementService.ts** ์ „ํ™˜ (35๊ฐœ ํ˜ธ์ถœ) +3. **dataflowService.ts** ์ „ํ™˜ (31๊ฐœ ํ˜ธ์ถœ) + +๋“ฑ ํ•ต์‹ฌ ์„œ๋น„์Šค๋ฅผ Raw Query๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +--- + +**์ž‘์„ฑ์ผ**: 2025-09-30 +**๋ฒ„์ „**: 1.0.0 +**๋‹ด๋‹น**: Backend Development Team \ No newline at end of file diff --git a/backend-node/src/database/db.ts b/backend-node/src/database/db.ts new file mode 100644 index 00000000..cd5f5142 --- /dev/null +++ b/backend-node/src/database/db.ts @@ -0,0 +1,271 @@ +/** + * PostgreSQL Raw Query ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งค๋‹ˆ์ € + * + * Prisma โ†’ Raw Query ์ „ํ™˜์˜ ํ•ต์‹ฌ ๋ชจ๋“ˆ + * - Connection Pool ๊ธฐ๋ฐ˜ ์•ˆ์ •์ ์ธ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ + * - ํŠธ๋žœ์žญ์…˜ ์ง€์› + * - ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ + * - ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋ฐ ์—๋Ÿฌ ํ•ธ๋“ค๋ง + */ + +import { + Pool, + PoolClient, + QueryResult as PgQueryResult, + QueryResultRow, +} from "pg"; +import config from "../config/environment"; + +// PostgreSQL ์—ฐ๊ฒฐ ํ’€ +let pool: Pool | null = null; + +/** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ’€ ์ดˆ๊ธฐํ™” + */ +export const initializePool = (): Pool => { + if (pool) { + return pool; + } + + // DATABASE_URL ํŒŒ์‹ฑ (postgresql://user:password@host:port/database) + const databaseUrl = config.databaseUrl; + + // URL ํŒŒ์‹ฑ ๋กœ์ง + const dbConfig = parseDatabaseUrl(databaseUrl); + + pool = new Pool({ + host: dbConfig.host, + port: dbConfig.port, + database: dbConfig.database, + user: dbConfig.user, + password: dbConfig.password, + + // ์—ฐ๊ฒฐ ํ’€ ์„ค์ • + min: config.nodeEnv === "production" ? 5 : 2, + max: config.nodeEnv === "production" ? 20 : 10, + + // ํƒ€์ž„์•„์›ƒ ์„ค์ • + connectionTimeoutMillis: 30000, // 30์ดˆ + idleTimeoutMillis: 600000, // 10๋ถ„ + + // ์—ฐ๊ฒฐ ์œ ์ง€ ์„ค์ • + keepAlive: true, + keepAliveInitialDelayMillis: 10000, + + // ์ฟผ๋ฆฌ ํƒ€์ž„์•„์›ƒ + statement_timeout: 60000, // 60์ดˆ (๋™์  ํ…Œ์ด๋ธ” ์ƒ์„ฑ ๋“ฑ ๊ณ ๋ ค) + query_timeout: 60000, + + // Application Name + application_name: "WACE-PLM-Backend", + }); + + // ์—ฐ๊ฒฐ ํ’€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ + pool.on("connect", (client) => { + if (config.debug) { + console.log("โœ… PostgreSQL ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์ƒ์„ฑ"); + } + }); + + pool.on("acquire", (client) => { + if (config.debug) { + console.log("๐Ÿ”’ PostgreSQL ํด๋ผ์ด์–ธํŠธ ํš๋“"); + } + }); + + pool.on("remove", (client) => { + if (config.debug) { + console.log("๐Ÿ—‘๏ธ PostgreSQL ํด๋ผ์ด์–ธํŠธ ์ œ๊ฑฐ"); + } + }); + + pool.on("error", (err, client) => { + console.error("โŒ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์—๋Ÿฌ:", err); + }); + + console.log( + `๐Ÿš€ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}` + ); + + return pool; +}; + +/** + * DATABASE_URL ํŒŒ์‹ฑ ํ—ฌํผ ํ•จ์ˆ˜ + */ +function parseDatabaseUrl(url: string) { + // postgresql://user:password@host:port/database + const regex = /postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/; + const match = url.match(regex); + + if (!match) { + // URL ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ + console.warn("โš ๏ธ DATABASE_URL ํŒŒ์‹ฑ ์‹คํŒจ, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ"); + return { + host: "localhost", + port: 5432, + database: "ilshin", + user: "postgres", + password: "postgres", + }; + } + + return { + user: decodeURIComponent(match[1]), + password: decodeURIComponent(match[2]), + host: match[3], + port: parseInt(match[4], 10), + database: match[5], + }; +} + +/** + * ์—ฐ๊ฒฐ ํ’€ ๊ฐ€์ ธ์˜ค๊ธฐ + */ +export const getPool = (): Pool => { + if (!pool) { + return initializePool(); + } + return pool; +}; + +/** + * ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ์‹คํ–‰ ํ•จ์ˆ˜ + * + * @param text SQL ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด (Parameterized Query) + * @param params ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐฐ์—ด + * @returns ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๋ฐฐ์—ด + * + * @example + * const users = await query('SELECT * FROM users WHERE user_id = $1', ['user123']); + */ +export async function query( + text: string, + params?: any[] +): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + const startTime = Date.now(); + const result: PgQueryResult = await client.query(text, params); + const duration = Date.now() - startTime; + + if (config.debug) { + console.log("๐Ÿ” ์ฟผ๋ฆฌ ์‹คํ–‰:", { + query: text, + params, + rowCount: result.rowCount, + duration: `${duration}ms`, + }); + } + + return result.rows; + } catch (error: any) { + console.error("โŒ ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹คํŒจ:", { + query: text, + params, + error: error.message, + }); + throw error; + } finally { + client.release(); + } +} + +/** + * ๋‹จ์ผ ํ–‰ ์กฐํšŒ ์ฟผ๋ฆฌ (๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜) + * + * @param text SQL ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด + * @param params ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ + * @returns ๋‹จ์ผ ํ–‰ ๋˜๋Š” null + * + * @example + * const user = await queryOne('SELECT * FROM users WHERE user_id = $1', ['user123']); + */ +export async function queryOne( + text: string, + params?: any[] +): Promise { + const rows = await query(text, params); + return rows.length > 0 ? rows[0] : null; +} + +/** + * ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ ํ•จ์ˆ˜ + * + * @param callback ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ + * @returns ์ฝœ๋ฐฑ ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’ + * + * @example + * const result = await transaction(async (client) => { + * await client.query('INSERT INTO users (...) VALUES (...)', []); + * await client.query('INSERT INTO user_roles (...) VALUES (...)', []); + * return { success: true }; + * }); + */ +export async function transaction( + callback: (client: PoolClient) => Promise +): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + if (config.debug) { + console.log("๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘"); + } + + const result = await callback(client); + + await client.query("COMMIT"); + + if (config.debug) { + console.log("โœ… ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์™„๋ฃŒ"); + } + + return result; + } catch (error: any) { + await client.query("ROLLBACK"); + + console.error("โŒ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ:", error.message); + throw error; + } finally { + client.release(); + } +} + +/** + * ์—ฐ๊ฒฐ ํ’€ ์ข…๋ฃŒ (์•ฑ ์ข…๋ฃŒ ์‹œ ํ˜ธ์ถœ) + */ +export async function closePool(): Promise { + if (pool) { + await pool.end(); + pool = null; + console.log("๐Ÿ›‘ PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์ข…๋ฃŒ"); + } +} + +/** + * ์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ ํ™•์ธ + */ +export function getPoolStatus() { + const pool = getPool(); + return { + totalCount: pool.totalCount, + idleCount: pool.idleCount, + waitingCount: pool.waitingCount, + }; +} + +// ๊ธฐ๋ณธ ์ต์ŠคํฌํŠธ (ํŽธ์˜์„ฑ) +export default { + query, + queryOne, + transaction, + getPool, + initializePool, + closePool, + getPoolStatus, +}; diff --git a/backend-node/src/services/externalCallConfigService.ts b/backend-node/src/services/externalCallConfigService.ts index d1120bc6..3fb60407 100644 --- a/backend-node/src/services/externalCallConfigService.ts +++ b/backend-node/src/services/externalCallConfigService.ts @@ -344,13 +344,14 @@ export class ExternalCallConfigService { } // 3. ์™ธ๋ถ€ API ํ˜ธ์ถœ - const callResult = await this.executeExternalCall(config, processedData, contextData); + const callResult = await this.executeExternalCall( + config, + processedData, + contextData + ); // 4. Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ (์žˆ๋Š” ๊ฒฝ์šฐ) - if ( - callResult.success && - configData?.dataMappingConfig?.inboundMapping - ) { + if (callResult.success && configData?.dataMappingConfig?.inboundMapping) { logger.info("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์ค‘..."); await this.processInboundMapping( configData.dataMappingConfig.inboundMapping, @@ -363,7 +364,7 @@ export class ExternalCallConfigService { return { success: callResult.success, - message: callResult.success + message: callResult.success ? `์™ธ๋ถ€ํ˜ธ์ถœ '${config.config_name}' ์‹คํ–‰ ์™„๋ฃŒ` : `์™ธ๋ถ€ํ˜ธ์ถœ '${config.config_name}' ์‹คํ–‰ ์‹คํŒจ`, data: callResult.data, @@ -373,9 +374,10 @@ export class ExternalCallConfigService { } catch (error) { const executionTime = performance.now() - startTime; logger.error("์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹คํŒจ:", error); - - const errorMessage = error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; - + + const errorMessage = + error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; + return { success: false, message: `์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ ์‹คํŒจ: ${errorMessage}`, @@ -388,14 +390,16 @@ export class ExternalCallConfigService { /** * ๐Ÿ”ฅ ๋ฒ„ํŠผ ์ œ์–ด์šฉ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ๋ชฉ๋ก ์กฐํšŒ (๊ฐ„์†Œํ™”๋œ ์ •๋ณด) */ - async getConfigsForButtonControl(companyCode: string): Promise> { + async getConfigsForButtonControl(companyCode: string): Promise< + Array<{ + id: string; + name: string; + description?: string; + apiUrl: string; + method: string; + hasDataMapping: boolean; + }> + > { try { const configs = await prisma.external_call_configs.findMany({ where: { @@ -421,7 +425,7 @@ export class ExternalCallConfigService { description: config.description || undefined, apiUrl: configData?.restApiSettings?.apiUrl || "", method: configData?.restApiSettings?.httpMethod || "GET", - hasDataMapping: !!(configData?.dataMappingConfig), + hasDataMapping: !!configData?.dataMappingConfig, }; }); } catch (error) { @@ -445,7 +449,12 @@ export class ExternalCallConfigService { throw new Error("REST API ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค."); } - const { apiUrl, httpMethod, headers = {}, timeout = 30000 } = restApiSettings; + const { + apiUrl, + httpMethod, + headers = {}, + timeout = 30000, + } = restApiSettings; // ์š”์ฒญ ํ—ค๋” ์ค€๋น„ const requestHeaders = { @@ -456,7 +465,9 @@ export class ExternalCallConfigService { // ์ธ์ฆ ์ฒ˜๋ฆฌ if (restApiSettings.authentication?.type === "basic") { const { username, password } = restApiSettings.authentication; - const credentials = Buffer.from(`${username}:${password}`).toString("base64"); + const credentials = Buffer.from(`${username}:${password}`).toString( + "base64" + ); requestHeaders["Authorization"] = `Basic ${credentials}`; } else if (restApiSettings.authentication?.type === "bearer") { const { token } = restApiSettings.authentication; @@ -488,14 +499,15 @@ export class ExternalCallConfigService { } const responseData = await response.json(); - + return { success: true, data: responseData, }; } catch (error) { logger.error("์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹คํŒจ:", error); - const errorMessage = error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; + const errorMessage = + error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"; return { success: false, error: errorMessage, @@ -517,9 +529,9 @@ export class ExternalCallConfigService { if (mapping.fieldMappings) { for (const fieldMapping of mapping.fieldMappings) { const { sourceField, targetField, transformation } = fieldMapping; - + let value = sourceData[sourceField]; - + // ๋ณ€ํ™˜ ๋กœ์ง ์ ์šฉ if (transformation) { switch (transformation.type) { @@ -534,7 +546,7 @@ export class ExternalCallConfigService { break; } } - + mappedData[targetField] = value; } } @@ -556,10 +568,9 @@ export class ExternalCallConfigService { try { // Inbound ๋งคํ•‘ ๋กœ์ง (์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด๋ถ€ ์‹œ์Šคํ…œ์— ์ €์žฅ) logger.info("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ:", mapping); - + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ๋‚ด๋ถ€ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๋กœ์ง ํ•„์š” // ์˜ˆ: ์™ธ๋ถ€ API์—์„œ ๋ฐ›์€ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‚ด๋ถ€ ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ”์— ์—…๋ฐ์ดํŠธ - } catch (error) { logger.error("Inbound ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ ์‹คํŒจ:", error); // Inbound ๋งคํ•‘ ์‹คํŒจ๋Š” ์ „์ฒด ํ”Œ๋กœ์šฐ๋ฅผ ์ค‘๋‹จํ•˜์ง€ ์•Š์Œ diff --git a/backend-node/src/services/multiConnectionQueryService.ts b/backend-node/src/services/multiConnectionQueryService.ts index 5ce9ca68..0751dfef 100644 --- a/backend-node/src/services/multiConnectionQueryService.ts +++ b/backend-node/src/services/multiConnectionQueryService.ts @@ -147,9 +147,9 @@ export class MultiConnectionQueryService { // INSERT ์ฟผ๋ฆฌ ๊ตฌ์„ฑ (DB ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ) const columns = Object.keys(data); let values = Object.values(data); - + // Oracle์˜ ๊ฒฝ์šฐ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ํ™•์ธ ๋ฐ ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜ ์ฒ˜๋ฆฌ - if (connection.db_type?.toLowerCase() === 'oracle') { + if (connection.db_type?.toLowerCase() === "oracle") { try { // Oracle ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์กฐํšŒ const schemaQuery = ` @@ -158,67 +158,80 @@ export class MultiConnectionQueryService { WHERE TABLE_NAME = UPPER('${tableName}') ORDER BY COLUMN_ID `; - + logger.info(`๐Ÿ” Oracle ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์กฐํšŒ: ${schemaQuery}`); - + const schemaResult = await ExternalDbConnectionService.executeQuery( connectionId, schemaQuery ); - + if (schemaResult.success && schemaResult.data) { logger.info(`๐Ÿ“‹ Oracle ํ…Œ์ด๋ธ” ${tableName} ์Šคํ‚ค๋งˆ:`); schemaResult.data.forEach((col: any) => { - logger.info(` - ${col.COLUMN_NAME}: ${col.DATA_TYPE}, NULL: ${col.NULLABLE}, DEFAULT: ${col.DATA_DEFAULT || 'None'}`); + logger.info( + ` - ${col.COLUMN_NAME}: ${col.DATA_TYPE}, NULL: ${col.NULLABLE}, DEFAULT: ${col.DATA_DEFAULT || "None"}` + ); }); - + // ํ•„์ˆ˜ ์ปฌ๋Ÿผ ์ค‘ ๋ˆ„๋ฝ๋œ ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ (๊ธฐ๋ณธ๊ฐ’์ด ์—†๋Š” NOT NULL ์ปฌ๋Ÿผ๋งŒ) - const providedColumns = columns.map(col => col.toUpperCase()); - const missingRequiredColumns = schemaResult.data.filter((schemaCol: any) => - schemaCol.NULLABLE === 'N' && - !schemaCol.DATA_DEFAULT && - !providedColumns.includes(schemaCol.COLUMN_NAME) + const providedColumns = columns.map((col) => col.toUpperCase()); + const missingRequiredColumns = schemaResult.data.filter( + (schemaCol: any) => + schemaCol.NULLABLE === "N" && + !schemaCol.DATA_DEFAULT && + !providedColumns.includes(schemaCol.COLUMN_NAME) ); - + if (missingRequiredColumns.length > 0) { - const missingNames = missingRequiredColumns.map((col: any) => col.COLUMN_NAME); - logger.error(`โŒ ํ•„์ˆ˜ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ: ${missingNames.join(', ')}`); - throw new Error(`ํ•„์ˆ˜ ์ปฌ๋Ÿผ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${missingNames.join(', ')}`); + const missingNames = missingRequiredColumns.map( + (col: any) => col.COLUMN_NAME + ); + logger.error(`โŒ ํ•„์ˆ˜ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ: ${missingNames.join(", ")}`); + throw new Error( + `ํ•„์ˆ˜ ์ปฌ๋Ÿผ์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${missingNames.join(", ")}` + ); } - - logger.info(`โœ… ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ํ†ต๊ณผ: ๋ชจ๋“  ํ•„์ˆ˜ ์ปฌ๋Ÿผ์ด ์ œ๊ณต๋˜์—ˆ๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’์ด ์žˆ์Šต๋‹ˆ๋‹ค.`); + + logger.info( + `โœ… ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ํ†ต๊ณผ: ๋ชจ๋“  ํ•„์ˆ˜ ์ปฌ๋Ÿผ์ด ์ œ๊ณต๋˜์—ˆ๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’์ด ์žˆ์Šต๋‹ˆ๋‹ค.` + ); } } catch (schemaError) { logger.warn(`โš ๏ธ ์Šคํ‚ค๋งˆ ์กฐํšŒ ์‹คํŒจ (๊ณ„์† ์ง„ํ–‰): ${schemaError}`); } - - values = values.map(value => { + + values = values.map((value) => { // null์ด๋‚˜ undefined๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ if (value === null || value === undefined) { return value; } - + // ์ˆซ์ž๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ ๋ฌธ์ž์—ด์€ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ - if (typeof value === 'string' && value.trim() !== '') { + if (typeof value === "string" && value.trim() !== "") { const numValue = Number(value); if (!isNaN(numValue)) { - logger.info(`๐Ÿ”„ Oracle ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜: "${value}" (string) โ†’ ${numValue} (number)`); + logger.info( + `๐Ÿ”„ Oracle ๋ฐ์ดํ„ฐ ํƒ€์ž… ๋ณ€ํ™˜: "${value}" (string) โ†’ ${numValue} (number)` + ); return numValue; } } - + return value; }); } - + let query: string; let queryParams: any[]; - const dbType = connection.db_type?.toLowerCase() || 'postgresql'; + const dbType = connection.db_type?.toLowerCase() || "postgresql"; switch (dbType) { - case 'oracle': + case "oracle": // Oracle: :1, :2 ์Šคํƒ€์ผ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ, RETURNING ๋ฏธ์ง€์› - const oraclePlaceholders = values.map((_, index) => `:${index + 1}`).join(", "); + const oraclePlaceholders = values + .map((_, index) => `:${index + 1}`) + .join(", "); query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${oraclePlaceholders})`; queryParams = values; logger.info(`๐Ÿ” Oracle INSERT ์ƒ์„ธ ์ •๋ณด:`); @@ -227,42 +240,57 @@ export class MultiConnectionQueryService { logger.info(` - ๊ฐ’: ${JSON.stringify(values)}`); logger.info(` - ์ฟผ๋ฆฌ: ${query}`); logger.info(` - ํŒŒ๋ผ๋ฏธํ„ฐ: ${JSON.stringify(queryParams)}`); - logger.info(` - ๋ฐ์ดํ„ฐ ํƒ€์ž…: ${JSON.stringify(values.map(v => typeof v))}`); + logger.info( + ` - ๋ฐ์ดํ„ฐ ํƒ€์ž…: ${JSON.stringify(values.map((v) => typeof v))}` + ); break; - case 'mysql': - case 'mariadb': + case "mysql": + case "mariadb": // MySQL/MariaDB: ? ์Šคํƒ€์ผ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ, RETURNING ๋ฏธ์ง€์› - const mysqlPlaceholders = values.map(() => '?').join(", "); + const mysqlPlaceholders = values.map(() => "?").join(", "); query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${mysqlPlaceholders})`; queryParams = values; - logger.info(`MySQL/MariaDB INSERT ์ฟผ๋ฆฌ:`, { query, params: queryParams }); + logger.info(`MySQL/MariaDB INSERT ์ฟผ๋ฆฌ:`, { + query, + params: queryParams, + }); break; - case 'sqlserver': - case 'mssql': + case "sqlserver": + case "mssql": // SQL Server: @param1, @param2 ์Šคํƒ€์ผ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ - const sqlServerPlaceholders = values.map((_, index) => `@param${index + 1}`).join(", "); + const sqlServerPlaceholders = values + .map((_, index) => `@param${index + 1}`) + .join(", "); query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${sqlServerPlaceholders})`; queryParams = values; - logger.info(`SQL Server INSERT ์ฟผ๋ฆฌ:`, { query, params: queryParams }); + logger.info(`SQL Server INSERT ์ฟผ๋ฆฌ:`, { + query, + params: queryParams, + }); break; - case 'sqlite': + case "sqlite": // SQLite: ? ์Šคํƒ€์ผ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ, RETURNING ์ง€์› (3.35.0+) - const sqlitePlaceholders = values.map(() => '?').join(", "); + const sqlitePlaceholders = values.map(() => "?").join(", "); query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${sqlitePlaceholders}) RETURNING *`; queryParams = values; logger.info(`SQLite INSERT ์ฟผ๋ฆฌ:`, { query, params: queryParams }); break; - case 'postgresql': + case "postgresql": default: // PostgreSQL: $1, $2 ์Šคํƒ€์ผ ๋ฐ”์ธ๋”ฉ ์‚ฌ์šฉ, RETURNING ์ง€์› - const pgPlaceholders = values.map((_, index) => `$${index + 1}`).join(", "); + const pgPlaceholders = values + .map((_, index) => `$${index + 1}`) + .join(", "); query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${pgPlaceholders}) RETURNING *`; queryParams = values; - logger.info(`PostgreSQL INSERT ์ฟผ๋ฆฌ:`, { query, params: queryParams }); + logger.info(`PostgreSQL INSERT ์ฟผ๋ฆฌ:`, { + query, + params: queryParams, + }); break; } diff --git a/backend-node/src/tests/database.test.ts b/backend-node/src/tests/database.test.ts new file mode 100644 index 00000000..dfd8251b --- /dev/null +++ b/backend-node/src/tests/database.test.ts @@ -0,0 +1,455 @@ +/** + * Database Manager ํ…Œ์ŠคํŠธ + * + * Phase 1 ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ ๊ฒ€์ฆ + */ + +import { query, queryOne, transaction, getPoolStatus } from "../database/db"; +import { QueryBuilder } from "../utils/queryBuilder"; +import { DatabaseValidator } from "../utils/databaseValidator"; + +describe("Database Manager Tests", () => { + describe("QueryBuilder", () => { + test("SELECT ์ฟผ๋ฆฌ ์ƒ์„ฑ - ๊ธฐ๋ณธ", () => { + const { query: sql, params } = QueryBuilder.select("users", { + where: { user_id: "test_user" }, + }); + + expect(sql).toContain("SELECT * FROM users"); + expect(sql).toContain("WHERE user_id = $1"); + expect(params).toEqual(["test_user"]); + }); + + test("SELECT ์ฟผ๋ฆฌ ์ƒ์„ฑ - ๋ณต์žกํ•œ ์กฐ๊ฑด", () => { + const { query: sql, params } = QueryBuilder.select("users", { + columns: ["user_id", "username", "email"], + where: { status: "active", role: "admin" }, + orderBy: "created_at DESC", + limit: 10, + offset: 20, + }); + + expect(sql).toContain("SELECT user_id, username, email FROM users"); + expect(sql).toContain("WHERE status = $1 AND role = $2"); + expect(sql).toContain("ORDER BY created_at DESC"); + expect(sql).toContain("LIMIT $3"); + expect(sql).toContain("OFFSET $4"); + expect(params).toEqual(["active", "admin", 10, 20]); + }); + + test("SELECT ์ฟผ๋ฆฌ ์ƒ์„ฑ - JOIN", () => { + const { query: sql, params } = QueryBuilder.select("users", { + columns: ["users.user_id", "users.username", "departments.dept_name"], + joins: [ + { + type: "LEFT", + table: "departments", + on: "users.dept_id = departments.dept_id", + }, + ], + where: { "users.status": "active" }, + }); + + expect(sql).toContain("LEFT JOIN departments"); + expect(sql).toContain("ON users.dept_id = departments.dept_id"); + expect(sql).toContain("WHERE users.status = $1"); + expect(params).toEqual(["active"]); + }); + + test("INSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ - RETURNING", () => { + const { query: sql, params } = QueryBuilder.insert( + "users", + { + user_id: "new_user", + username: "John Doe", + email: "john@example.com", + }, + { + returning: ["id", "user_id"], + } + ); + + expect(sql).toContain("INSERT INTO users"); + expect(sql).toContain("(user_id, username, email)"); + expect(sql).toContain("VALUES ($1, $2, $3)"); + expect(sql).toContain("RETURNING id, user_id"); + expect(params).toEqual(["new_user", "John Doe", "john@example.com"]); + }); + + test("INSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ - UPSERT", () => { + const { query: sql, params } = QueryBuilder.insert( + "users", + { + user_id: "user123", + username: "Jane", + email: "jane@example.com", + }, + { + onConflict: { + columns: ["user_id"], + action: "DO UPDATE", + updateSet: ["username", "email"], + }, + returning: ["*"], + } + ); + + expect(sql).toContain("ON CONFLICT (user_id) DO UPDATE"); + expect(sql).toContain( + "SET username = EXCLUDED.username, email = EXCLUDED.email" + ); + expect(sql).toContain("RETURNING *"); + }); + + test("UPDATE ์ฟผ๋ฆฌ ์ƒ์„ฑ", () => { + const { query: sql, params } = QueryBuilder.update( + "users", + { username: "Updated Name", email: "updated@example.com" }, + { user_id: "user123" }, + { returning: ["*"] } + ); + + expect(sql).toContain("UPDATE users"); + expect(sql).toContain("SET username = $1, email = $2"); + expect(sql).toContain("WHERE user_id = $3"); + expect(sql).toContain("RETURNING *"); + expect(params).toEqual([ + "Updated Name", + "updated@example.com", + "user123", + ]); + }); + + test("DELETE ์ฟผ๋ฆฌ ์ƒ์„ฑ", () => { + const { query: sql, params } = QueryBuilder.delete("users", { + user_id: "user_to_delete", + }); + + expect(sql).toContain("DELETE FROM users"); + expect(sql).toContain("WHERE user_id = $1"); + expect(params).toEqual(["user_to_delete"]); + }); + + test("COUNT ์ฟผ๋ฆฌ ์ƒ์„ฑ", () => { + const { query: sql, params } = QueryBuilder.count("users", { + status: "active", + }); + + expect(sql).toContain("SELECT COUNT(*) as count FROM users"); + expect(sql).toContain("WHERE status = $1"); + expect(params).toEqual(["active"]); + }); + }); + + describe("DatabaseValidator", () => { + test("ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ - ์œ ํšจํ•œ ์ด๋ฆ„", () => { + expect(DatabaseValidator.validateTableName("users")).toBe(true); + expect(DatabaseValidator.validateTableName("user_info")).toBe(true); + expect(DatabaseValidator.validateTableName("_internal_table")).toBe(true); + expect(DatabaseValidator.validateTableName("table123")).toBe(true); + }); + + test("ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ - ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฆ„", () => { + expect(DatabaseValidator.validateTableName("")).toBe(false); + expect(DatabaseValidator.validateTableName("123table")).toBe(false); + expect(DatabaseValidator.validateTableName("user-table")).toBe(false); + expect(DatabaseValidator.validateTableName("user table")).toBe(false); + expect(DatabaseValidator.validateTableName("SELECT")).toBe(false); // ์˜ˆ์•ฝ์–ด + expect(DatabaseValidator.validateTableName("a".repeat(64))).toBe(false); // ๋„ˆ๋ฌด ๊ธบ + }); + + test("์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ - ์œ ํšจํ•œ ์ด๋ฆ„", () => { + expect(DatabaseValidator.validateColumnName("user_id")).toBe(true); + expect(DatabaseValidator.validateColumnName("created_at")).toBe(true); + expect(DatabaseValidator.validateColumnName("is_active")).toBe(true); + }); + + test("์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ - ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฆ„", () => { + expect(DatabaseValidator.validateColumnName("user-id")).toBe(false); + expect(DatabaseValidator.validateColumnName("user id")).toBe(false); + expect(DatabaseValidator.validateColumnName("WHERE")).toBe(false); // ์˜ˆ์•ฝ์–ด + }); + + test("๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ฒ€์ฆ", () => { + expect(DatabaseValidator.validateDataType("VARCHAR")).toBe(true); + expect(DatabaseValidator.validateDataType("VARCHAR(255)")).toBe(true); + expect(DatabaseValidator.validateDataType("INTEGER")).toBe(true); + expect(DatabaseValidator.validateDataType("TIMESTAMP")).toBe(true); + expect(DatabaseValidator.validateDataType("JSONB")).toBe(true); + expect(DatabaseValidator.validateDataType("INTEGER[]")).toBe(true); + expect(DatabaseValidator.validateDataType("DECIMAL(10,2)")).toBe(true); + }); + + test("WHERE ์กฐ๊ฑด ๊ฒ€์ฆ", () => { + expect( + DatabaseValidator.validateWhereClause({ + user_id: "test", + status: "active", + }) + ).toBe(true); + + expect( + DatabaseValidator.validateWhereClause({ + "config->>type": "form", // JSON ์ฟผ๋ฆฌ + }) + ).toBe(true); + + expect( + DatabaseValidator.validateWhereClause({ + "invalid-column": "value", + }) + ).toBe(false); + }); + + test("ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ฒ€์ฆ", () => { + expect(DatabaseValidator.validatePagination(1, 10)).toBe(true); + expect(DatabaseValidator.validatePagination(5, 100)).toBe(true); + + expect(DatabaseValidator.validatePagination(0, 10)).toBe(false); // page < 1 + expect(DatabaseValidator.validatePagination(1, 0)).toBe(false); // pageSize < 1 + expect(DatabaseValidator.validatePagination(1, 2000)).toBe(false); // pageSize > 1000 + }); + + test("ORDER BY ๊ฒ€์ฆ", () => { + expect(DatabaseValidator.validateOrderBy("created_at")).toBe(true); + expect(DatabaseValidator.validateOrderBy("created_at ASC")).toBe(true); + expect(DatabaseValidator.validateOrderBy("created_at DESC")).toBe(true); + + expect(DatabaseValidator.validateOrderBy("created_at INVALID")).toBe( + false + ); + expect(DatabaseValidator.validateOrderBy("invalid-column ASC")).toBe( + false + ); + }); + + test("UUID ๊ฒ€์ฆ", () => { + expect( + DatabaseValidator.validateUUID("550e8400-e29b-41d4-a716-446655440000") + ).toBe(true); + expect(DatabaseValidator.validateUUID("invalid-uuid")).toBe(false); + }); + + test("์ด๋ฉ”์ผ ๊ฒ€์ฆ", () => { + expect(DatabaseValidator.validateEmail("test@example.com")).toBe(true); + expect(DatabaseValidator.validateEmail("user.name@domain.co.kr")).toBe( + true + ); + expect(DatabaseValidator.validateEmail("invalid-email")).toBe(false); + expect(DatabaseValidator.validateEmail("test@")).toBe(false); + }); + }); + + describe("Integration Tests (์‹ค์ œ DB ์—ฐ๊ฒฐ ํ•„์š”)", () => { + // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋“ค + // DB ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ์Šคํ‚ต๋˜๋„๋ก ์„ค์ • + + beforeAll(async () => { + // DB ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ + try { + await query("SELECT 1 as test"); + console.log("โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต - Integration Tests ์‹คํ–‰"); + } catch (error) { + console.warn("โš ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ - Integration Tests ์Šคํ‚ต"); + console.warn("DB ์—ฐ๊ฒฐ ์˜ค๋ฅ˜:", error); + } + }); + + test("์‹ค์ œ ์ฟผ๋ฆฌ ์‹คํ–‰ ํ…Œ์ŠคํŠธ", async () => { + try { + const result = await query( + "SELECT NOW() as current_time, version() as pg_version" + ); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("current_time"); + expect(result[0]).toHaveProperty("pg_version"); + expect(result[0].pg_version).toContain("PostgreSQL"); + + console.log("๐Ÿ• ํ˜„์žฌ ์‹œ๊ฐ„:", result[0].current_time); + console.log("๐Ÿ“Š PostgreSQL ๋ฒ„์ „:", result[0].pg_version); + } catch (error) { + console.error("โŒ ์ฟผ๋ฆฌ ์‹คํ–‰ ํ…Œ์ŠคํŠธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ", async () => { + try { + const testValue = "test_value_" + Date.now(); + const result = await query( + "SELECT $1 as input_value, $2 as number_value, $3 as boolean_value", + [testValue, 42, true] + ); + + expect(result).toHaveLength(1); + expect(result[0].input_value).toBe(testValue); + expect(parseInt(result[0].number_value)).toBe(42); // PostgreSQL์€ ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ + expect( + result[0].boolean_value === true || result[0].boolean_value === "true" + ).toBe(true); // PostgreSQL boolean ์ฒ˜๋ฆฌ + + console.log("๐Ÿ“ ํŒŒ๋ผ๋ฏธํ„ฐ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ:", result[0]); + } catch (error) { + console.error("โŒ ํŒŒ๋ผ๋ฏธํ„ฐ ์ฟผ๋ฆฌ ํ…Œ์ŠคํŠธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("๋‹จ์ผ ํ–‰ ์กฐํšŒ ํ…Œ์ŠคํŠธ", async () => { + try { + // ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์กฐํšŒ + const result = await queryOne("SELECT 1 as value, 'exists' as status"); + expect(result).not.toBeNull(); + expect(result?.value).toBe(1); + expect(result?.status).toBe("exists"); + + // ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ ์กฐํšŒ + const emptyResult = await queryOne( + "SELECT * FROM (SELECT 1 as id) t WHERE id = 999" + ); + expect(emptyResult).toBeNull(); + + console.log("๐Ÿ” ๋‹จ์ผ ํ–‰ ์กฐํšŒ ๊ฒฐ๊ณผ:", result); + } catch (error) { + console.error("โŒ ๋‹จ์ผ ํ–‰ ์กฐํšŒ ํ…Œ์ŠคํŠธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("ํŠธ๋žœ์žญ์…˜ ํ…Œ์ŠคํŠธ", async () => { + try { + const result = await transaction(async (client) => { + const res1 = await client.query( + "SELECT 1 as value, 'first' as label" + ); + const res2 = await client.query( + "SELECT 2 as value, 'second' as label" + ); + const res3 = await client.query("SELECT $1 as computed_value", [ + res1.rows[0].value + res2.rows[0].value, + ]); + + return { + res1: res1.rows, + res2: res2.rows, + res3: res3.rows, + transaction_id: Math.random().toString(36).substr(2, 9), + }; + }); + + expect(result.res1[0].value).toBe(1); + expect(result.res1[0].label).toBe("first"); + expect(result.res2[0].value).toBe(2); + expect(result.res2[0].label).toBe("second"); + expect(parseInt(result.res3[0].computed_value)).toBe(3); // PostgreSQL์€ ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ + expect(result.transaction_id).toBeDefined(); + + console.log("๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ:", { + first_value: result.res1[0].value, + second_value: result.res2[0].value, + computed_value: result.res3[0].computed_value, + transaction_id: result.transaction_id, + }); + } catch (error) { + console.error("โŒ ํŠธ๋žœ์žญ์…˜ ํ…Œ์ŠคํŠธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ", async () => { + try { + await expect( + transaction(async (client) => { + await client.query("SELECT 1 as value"); + // ์˜๋„์ ์œผ๋กœ ์˜ค๋ฅ˜ ๋ฐœ์ƒ + throw new Error("์˜๋„์ ์ธ ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ"); + }) + ).rejects.toThrow("์˜๋„์ ์ธ ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ"); + + console.log("๐Ÿ”„ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ ์„ฑ๊ณต"); + } catch (error) { + console.error("โŒ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ํ…Œ์ŠคํŠธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ ํ™•์ธ", () => { + try { + const status = getPoolStatus(); + + expect(status).toHaveProperty("totalCount"); + expect(status).toHaveProperty("idleCount"); + expect(status).toHaveProperty("waitingCount"); + expect(typeof status.totalCount).toBe("number"); + expect(typeof status.idleCount).toBe("number"); + expect(typeof status.waitingCount).toBe("number"); + + console.log("๐ŸŠโ€โ™‚๏ธ ์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ:", { + ์ด_์—ฐ๊ฒฐ์ˆ˜: status.totalCount, + ์œ ํœด_์—ฐ๊ฒฐ์ˆ˜: status.idleCount, + ๋Œ€๊ธฐ_์—ฐ๊ฒฐ์ˆ˜: status.waitingCount, + }); + } catch (error) { + console.error("โŒ ์—ฐ๊ฒฐ ํ’€ ์ƒํƒœ ํ™•์ธ ์‹คํŒจ:", error); + throw error; + } + }); + + test("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ", async () => { + try { + // ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •๋ณด ์กฐํšŒ + const dbInfo = await query(` + SELECT + current_database() as database_name, + current_user as current_user, + inet_server_addr() as server_address, + inet_server_port() as server_port + `); + + expect(dbInfo).toHaveLength(1); + expect(dbInfo[0].database_name).toBeDefined(); + expect(dbInfo[0].current_user).toBeDefined(); + + console.log("๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ •๋ณด:", { + ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ช…: dbInfo[0].database_name, + ํ˜„์žฌ์‚ฌ์šฉ์ž: dbInfo[0].current_user, + ์„œ๋ฒ„์ฃผ์†Œ: dbInfo[0].server_address, + ์„œ๋ฒ„ํฌํŠธ: dbInfo[0].server_port, + }); + } catch (error) { + console.error("โŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹คํŒจ:", error); + throw error; + } + }); + + test("ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ", async () => { + try { + // ์‹œ์Šคํ…œ ํ…Œ์ด๋ธ” ์กฐํšŒ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ + const tables = await query(` + SELECT table_name, table_type + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + LIMIT 5 + `); + + expect(Array.isArray(tables)).toBe(true); + console.log(`๐Ÿ“‹ ๋ฐœ๊ฒฌ๋œ ํ…Œ์ด๋ธ” ์ˆ˜: ${tables.length}`); + + if (tables.length > 0) { + console.log( + "๐Ÿ“‹ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (์ตœ๋Œ€ 5๊ฐœ):", + tables.map((t) => t.table_name).join(", ") + ); + } + } catch (error) { + console.error("โŒ ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ ์‹คํŒจ:", error); + throw error; + } + }); + }); +}); + +// ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋ฐฉ๋ฒ•: +// npm test -- database.test.ts diff --git a/backend-node/src/tests/env.setup.ts b/backend-node/src/tests/env.setup.ts new file mode 100644 index 00000000..55263a9a --- /dev/null +++ b/backend-node/src/tests/env.setup.ts @@ -0,0 +1,18 @@ +/** + * Jest ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + */ + +// ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • +process.env.NODE_ENV = "test"; +// ์‹ค์ œ DB ์—ฐ๊ฒฐ์„ ์œ„ํ•ด ์šด์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ (์ฝ๊ธฐ ์ „์šฉ ํ…Œ์ŠคํŠธ๋งŒ ์ˆ˜ํ–‰) +process.env.DATABASE_URL = + process.env.TEST_DATABASE_URL || + "postgresql://postgres:ph0909!!@39.117.244.52:11132/plm"; +process.env.JWT_SECRET = "test-jwt-secret-key-for-testing-only"; +process.env.PORT = "3001"; +process.env.DEBUG = "true"; // ํ…Œ์ŠคํŠธ ์‹œ ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ํ™œ์„ฑํ™” + +// ์ฝ˜์†” ๋กœ๊ทธ ์ตœ์†Œํ™” (ํ•„์š”์‹œ ์ฃผ์„ ํ•ด์ œ) +// console.log = jest.fn(); +// console.warn = jest.fn(); +// console.error = jest.fn(); diff --git a/backend-node/src/tests/setup.ts b/backend-node/src/tests/setup.ts new file mode 100644 index 00000000..1b50e163 --- /dev/null +++ b/backend-node/src/tests/setup.ts @@ -0,0 +1,24 @@ +/** + * Jest ํ…Œ์ŠคํŠธ ์„ค์ • ๋ฐ ์ดˆ๊ธฐํ™” + */ + +import { closePool } from "../database/db"; + +// ํ…Œ์ŠคํŠธ ์™„๋ฃŒ ํ›„ ์ •๋ฆฌ +afterAll(async () => { + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ’€ ์ข…๋ฃŒ + await closePool(); +}); + +// ํ…Œ์ŠคํŠธ ํƒ€์ž„์•„์›ƒ ์„ค์ • +jest.setTimeout(30000); + +// ์ „์—ญ ํ…Œ์ŠคํŠธ ์„ค์ • +beforeEach(() => { + // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ์‹คํ–‰ํ•  ์„ค์ • +}); + +afterEach(() => { + // ๊ฐ ํ…Œ์ŠคํŠธ ํ›„์— ์‹คํ–‰ํ•  ์ •๋ฆฌ +}); + diff --git a/backend-node/src/types/database.ts b/backend-node/src/types/database.ts new file mode 100644 index 00000000..b4ba0bbc --- /dev/null +++ b/backend-node/src/types/database.ts @@ -0,0 +1,207 @@ +/** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ํƒ€์ž… ์ •์˜ + * + * Raw Query ๊ธฐ๋ฐ˜ ํƒ€์ž… ์•ˆ์ „์„ฑ ๋ณด์žฅ + */ + +/** + * ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ธํ„ฐํŽ˜์ด์Šค + */ +export interface QueryResult { + rows: T[]; + rowCount: number | null; + command: string; + fields?: any[]; +} + +/** + * ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€ + */ +export enum IsolationLevel { + READ_UNCOMMITTED = 'READ UNCOMMITTED', + READ_COMMITTED = 'READ COMMITTED', + REPEATABLE_READ = 'REPEATABLE READ', + SERIALIZABLE = 'SERIALIZABLE', +} + +/** + * ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ •์˜ + */ +export interface TableSchema { + tableName: string; + columns: ColumnDefinition[]; + constraints?: TableConstraint[]; + indexes?: IndexDefinition[]; + comment?: string; +} + +/** + * ์ปฌ๋Ÿผ ์ •์˜ + */ +export interface ColumnDefinition { + name: string; + type: PostgreSQLDataType; + nullable?: boolean; + defaultValue?: string; + isPrimaryKey?: boolean; + isUnique?: boolean; + references?: ForeignKeyReference; + comment?: string; +} + +/** + * PostgreSQL ๋ฐ์ดํ„ฐ ํƒ€์ž… + */ +export type PostgreSQLDataType = + // ์ˆซ์ž ํƒ€์ž… + | 'SMALLINT' + | 'INTEGER' + | 'BIGINT' + | 'DECIMAL' + | 'NUMERIC' + | 'REAL' + | 'DOUBLE PRECISION' + | 'SERIAL' + | 'BIGSERIAL' + + // ๋ฌธ์ž์—ด ํƒ€์ž… + | 'CHARACTER VARYING' // VARCHAR + | 'VARCHAR' + | 'CHARACTER' + | 'CHAR' + | 'TEXT' + + // ๋‚ ์งœ/์‹œ๊ฐ„ ํƒ€์ž… + | 'TIMESTAMP' + | 'TIMESTAMP WITH TIME ZONE' + | 'TIMESTAMPTZ' + | 'DATE' + | 'TIME' + | 'TIME WITH TIME ZONE' + | 'INTERVAL' + + // Boolean + | 'BOOLEAN' + + // JSON + | 'JSON' + | 'JSONB' + + // UUID + | 'UUID' + + // ๋ฐฐ์—ด + | 'ARRAY' + + // ๊ธฐํƒ€ + | 'BYTEA' + | string; // ์ปค์Šคํ…€ ํƒ€์ž… ํ—ˆ์šฉ + +/** + * ์™ธ๋ž˜ ํ‚ค ์ฐธ์กฐ + */ +export interface ForeignKeyReference { + table: string; + column: string; + onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'; + onUpdate?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'; +} + +/** + * ํ…Œ์ด๋ธ” ์ œ์•ฝ ์กฐ๊ฑด + */ +export interface TableConstraint { + name: string; + type: 'PRIMARY KEY' | 'FOREIGN KEY' | 'UNIQUE' | 'CHECK'; + columns: string[]; + references?: ForeignKeyReference; + checkExpression?: string; +} + +/** + * ์ธ๋ฑ์Šค ์ •์˜ + */ +export interface IndexDefinition { + name: string; + columns: string[]; + unique?: boolean; + type?: 'BTREE' | 'HASH' | 'GIN' | 'GIST'; + where?: string; // Partial index +} + +/** + * ์ฟผ๋ฆฌ ์‹คํ–‰ ์˜ต์…˜ + */ +export interface QueryOptions { + timeout?: number; + preparedStatement?: boolean; + rowMode?: 'array' | 'object'; +} + +/** + * ๋™์  ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์š”์ฒญ + */ +export interface DynamicTableRequest { + tableName: string; + columns: ColumnDefinition[]; + constraints?: TableConstraint[]; + indexes?: IndexDefinition[]; + ifNotExists?: boolean; + comment?: string; +} + +/** + * ๋™์  ํ…Œ์ด๋ธ” ์ˆ˜์ • ์š”์ฒญ + */ +export interface AlterTableRequest { + tableName: string; + operations: AlterTableOperation[]; +} + +/** + * ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์ž‘์—… + */ +export type AlterTableOperation = + | { type: 'ADD_COLUMN'; column: ColumnDefinition } + | { type: 'DROP_COLUMN'; columnName: string } + | { type: 'ALTER_COLUMN'; columnName: string; newDefinition: Partial } + | { type: 'RENAME_COLUMN'; oldName: string; newName: string } + | { type: 'ADD_CONSTRAINT'; constraint: TableConstraint } + | { type: 'DROP_CONSTRAINT'; constraintName: string }; + +/** + * ํŽ˜์ด์ง€๋„ค์ด์…˜ ์š”์ฒญ + */ +export interface PaginationRequest { + page: number; + pageSize: number; + orderBy?: string; + orderDirection?: 'ASC' | 'DESC'; +} + +/** + * ํŽ˜์ด์ง€๋„ค์ด์…˜ ์‘๋‹ต + */ +export interface PaginationResponse { + data: T[]; + pagination: { + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; +} + +/** + * ์ฟผ๋ฆฌ ํ†ต๊ณ„ + */ +export interface QueryStatistics { + query: string; + executionTime: number; + rowsAffected: number; + timestamp: Date; + success: boolean; + error?: string; +} \ No newline at end of file diff --git a/backend-node/src/utils/databaseValidator.ts b/backend-node/src/utils/databaseValidator.ts new file mode 100644 index 00000000..3e61072d --- /dev/null +++ b/backend-node/src/utils/databaseValidator.ts @@ -0,0 +1,383 @@ +/** + * ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ ๊ฒ€์ฆ ์œ ํ‹ธ๋ฆฌํ‹ฐ + * + * SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•œ ๊ฒ€์ฆ ํ•จ์ˆ˜๋“ค + */ + +export class DatabaseValidator { + // PostgreSQL ์˜ˆ์•ฝ์–ด ๋ชฉ๋ก (์ฃผ์š” ํ‚ค์›Œ๋“œ๋งŒ) + private static readonly RESERVED_WORDS = new Set([ + "SELECT", + "INSERT", + "UPDATE", + "DELETE", + "FROM", + "WHERE", + "JOIN", + "INNER", + "LEFT", + "RIGHT", + "FULL", + "ON", + "GROUP", + "BY", + "ORDER", + "HAVING", + "LIMIT", + "OFFSET", + "UNION", + "ALL", + "DISTINCT", + "AS", + "AND", + "OR", + "NOT", + "NULL", + "TRUE", + "FALSE", + "CASE", + "WHEN", + "THEN", + "ELSE", + "END", + "IF", + "EXISTS", + "IN", + "BETWEEN", + "LIKE", + "ILIKE", + "SIMILAR", + "TO", + "CREATE", + "DROP", + "ALTER", + "TABLE", + "INDEX", + "VIEW", + "FUNCTION", + "PROCEDURE", + "TRIGGER", + "DATABASE", + "SCHEMA", + "USER", + "ROLE", + "GRANT", + "REVOKE", + "COMMIT", + "ROLLBACK", + "BEGIN", + "TRANSACTION", + "SAVEPOINT", + "RELEASE", + "CONSTRAINT", + "PRIMARY", + "FOREIGN", + "KEY", + "UNIQUE", + "CHECK", + "DEFAULT", + "REFERENCES", + "CASCADE", + "RESTRICT", + "SET", + "ACTION", + "DEFERRABLE", + "INITIALLY", + "DEFERRED", + "IMMEDIATE", + "MATCH", + "PARTIAL", + "SIMPLE", + "FULL", + ]); + + // ์œ ํšจํ•œ PostgreSQL ๋ฐ์ดํ„ฐ ํƒ€์ž… ํŒจํ„ด + private static readonly DATA_TYPE_PATTERNS = [ + /^(SMALLINT|INTEGER|BIGINT|DECIMAL|NUMERIC|REAL|DOUBLE\s+PRECISION|SMALLSERIAL|SERIAL|BIGSERIAL)$/i, + /^(MONEY)$/i, + /^(CHARACTER\s+VARYING|VARCHAR|CHARACTER|CHAR|TEXT)(\(\d+\))?$/i, + /^(BYTEA)$/i, + /^(TIMESTAMP|TIME)(\s+(WITH|WITHOUT)\s+TIME\s+ZONE)?(\(\d+\))?$/i, + /^(DATE|INTERVAL)(\(\d+\))?$/i, + /^(BOOLEAN|BOOL)$/i, + /^(POINT|LINE|LSEG|BOX|PATH|POLYGON|CIRCLE)$/i, + /^(CIDR|INET|MACADDR|MACADDR8)$/i, + /^(BIT|BIT\s+VARYING)(\(\d+\))?$/i, + /^(TSVECTOR|TSQUERY)$/i, + /^(UUID)$/i, + /^(XML)$/i, + /^(JSON|JSONB)$/i, + /^(ARRAY|INTEGER\[\]|TEXT\[\]|VARCHAR\[\])$/i, + /^(DECIMAL|NUMERIC)\(\d+,\d+\)$/i, + ]; + + /** + * ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ + */ + static validateTableName(tableName: string): boolean { + if (!tableName || typeof tableName !== "string") { + return false; + } + + // ๊ธธ์ด ์ œํ•œ (PostgreSQL ์ตœ๋Œ€ 63์ž) + if (tableName.length === 0 || tableName.length > 63) { + return false; + } + + // ์œ ํšจํ•œ ์‹๋ณ„์ž ํŒจํ„ด (๋ฌธ์ž ๋˜๋Š” ๋ฐ‘์ค„๋กœ ์‹œ์ž‘, ๋ฌธ์ž/์ˆซ์ž/๋ฐ‘์ค„๋งŒ ํฌํ•จ) + const validPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + if (!validPattern.test(tableName)) { + return false; + } + + // ์˜ˆ์•ฝ์–ด ์ฒดํฌ + if (this.RESERVED_WORDS.has(tableName.toUpperCase())) { + return false; + } + + return true; + } + + /** + * ์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ + */ + static validateColumnName(columnName: string): boolean { + if (!columnName || typeof columnName !== "string") { + return false; + } + + // ๊ธธ์ด ์ œํ•œ + if (columnName.length === 0 || columnName.length > 63) { + return false; + } + + // JSON ์—ฐ์‚ฐ์ž ํฌํ•จ ์ปฌ๋Ÿผ๋ช… ํ—ˆ์šฉ (์˜ˆ: config->>'type', data->>path) + if (columnName.includes("->") || columnName.includes("->>")) { + const baseName = columnName.split(/->|->>/)[0]; + return this.validateColumnName(baseName); + } + + // ์œ ํšจํ•œ ์‹๋ณ„์ž ํŒจํ„ด + const validPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + if (!validPattern.test(columnName)) { + return false; + } + + // ์˜ˆ์•ฝ์–ด ์ฒดํฌ + if (this.RESERVED_WORDS.has(columnName.toUpperCase())) { + return false; + } + + return true; + } + + /** + * ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ฒ€์ฆ + */ + static validateDataType(dataType: string): boolean { + if (!dataType || typeof dataType !== "string") { + return false; + } + + const normalizedType = dataType.trim().toUpperCase(); + + return this.DATA_TYPE_PATTERNS.some((pattern) => + pattern.test(normalizedType) + ); + } + + /** + * WHERE ์กฐ๊ฑด ๊ฒ€์ฆ + */ + static validateWhereClause(whereClause: Record): boolean { + if (!whereClause || typeof whereClause !== "object") { + return false; + } + + // ๋ชจ๋“  ํ‚ค๊ฐ€ ์œ ํšจํ•œ ์ปฌ๋Ÿผ๋ช…์ธ์ง€ ํ™•์ธ + for (const key of Object.keys(whereClause)) { + if (!this.validateColumnName(key)) { + return false; + } + } + + return true; + } + + /** + * ํŽ˜์ด์ง€๋„ค์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฒ€์ฆ + */ + static validatePagination(page: number, pageSize: number): boolean { + // ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋Š” 1 ์ด์ƒ + if (!Number.isInteger(page) || page < 1) { + return false; + } + + // ํŽ˜์ด์ง€ ํฌ๊ธฐ๋Š” 1 ์ด์ƒ 1000 ์ดํ•˜ + if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1000) { + return false; + } + + return true; + } + + /** + * ORDER BY ์ ˆ ๊ฒ€์ฆ + */ + static validateOrderBy(orderBy: string): boolean { + if (!orderBy || typeof orderBy !== "string") { + return false; + } + + // ๊ธฐ๋ณธ ํŒจํ„ด: column_name [ASC|DESC] + const orderPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\s+(ASC|DESC))?$/i; + + // ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ ์ •๋ ฌ์˜ ๊ฒฝ์šฐ ์ฝค๋งˆ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ๊ฐ ๊ฒ€์ฆ + const orderClauses = orderBy.split(",").map((clause) => clause.trim()); + + return orderClauses.every((clause) => { + return ( + orderPattern.test(clause) && + this.validateColumnName(clause.split(/\s+/)[0]) + ); + }); + } + + /** + * UUID ํ˜•์‹ ๊ฒ€์ฆ + */ + static validateUUID(uuid: string): boolean { + if (!uuid || typeof uuid !== "string") { + return false; + } + + const uuidPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidPattern.test(uuid); + } + + /** + * ์ด๋ฉ”์ผ ํ˜•์‹ ๊ฒ€์ฆ + */ + static validateEmail(email: string): boolean { + if (!email || typeof email !== "string") { + return false; + } + + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailPattern.test(email) && email.length <= 254; + } + + /** + * SQL ์ธ์ ์…˜ ์œ„ํ—˜ ๋ฌธ์ž์—ด ๊ฒ€์‚ฌ + */ + static containsSqlInjection(input: string): boolean { + if (!input || typeof input !== "string") { + return false; + } + + // ์œ„ํ—˜ํ•œ SQL ํŒจํ„ด๋“ค + const dangerousPatterns = [ + /('|\\')|(;)|(--)|(\s+(OR|AND)\s+\d+\s*=\s*\d+)/i, + /(UNION|SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)/i, + /(\bxp_\w+|\bsp_\w+)/i, // SQL Server ํ™•์žฅ ํ”„๋กœ์‹œ์ € + /(script|javascript|vbscript|onload|onerror)/i, // XSS ํŒจํ„ด + ]; + + return dangerousPatterns.some((pattern) => pattern.test(input)); + } + + /** + * ์ˆซ์ž ๋ฒ”์œ„ ๊ฒ€์ฆ + */ + static validateNumberRange( + value: number, + min?: number, + max?: number + ): boolean { + if (typeof value !== "number" || !Number.isFinite(value)) { + return false; + } + + if (min !== undefined && value < min) { + return false; + } + + if (max !== undefined && value > max) { + return false; + } + + return true; + } + + /** + * ๋ฌธ์ž์—ด ๊ธธ์ด ๊ฒ€์ฆ + */ + static validateStringLength( + value: string, + minLength?: number, + maxLength?: number + ): boolean { + if (typeof value !== "string") { + return false; + } + + if (minLength !== undefined && value.length < minLength) { + return false; + } + + if (maxLength !== undefined && value.length > maxLength) { + return false; + } + + return true; + } + + /** + * JSON ํ˜•์‹ ๊ฒ€์ฆ + */ + static validateJSON(jsonString: string): boolean { + try { + JSON.parse(jsonString); + return true; + } catch { + return false; + } + } + + /** + * ๋‚ ์งœ ํ˜•์‹ ๊ฒ€์ฆ (ISO 8601) + */ + static validateDateISO(dateString: string): boolean { + if (!dateString || typeof dateString !== "string") { + return false; + } + + const date = new Date(dateString); + return !isNaN(date.getTime()) && dateString === date.toISOString(); + } + + /** + * ๋ฐฐ์—ด ์š”์†Œ ๊ฒ€์ฆ + */ + static validateArray( + array: any[], + validator: (item: T) => boolean, + minLength?: number, + maxLength?: number + ): boolean { + if (!Array.isArray(array)) { + return false; + } + + if (minLength !== undefined && array.length < minLength) { + return false; + } + + if (maxLength !== undefined && array.length > maxLength) { + return false; + } + + return array.every((item) => validator(item)); + } +} diff --git a/backend-node/src/utils/queryBuilder.ts b/backend-node/src/utils/queryBuilder.ts new file mode 100644 index 00000000..b83da5cb --- /dev/null +++ b/backend-node/src/utils/queryBuilder.ts @@ -0,0 +1,287 @@ +/** + * SQL ์ฟผ๋ฆฌ ๋นŒ๋” ์œ ํ‹ธ๋ฆฌํ‹ฐ + * + * Raw Query ๋ฐฉ์‹์—์„œ ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ธ ์ฟผ๋ฆฌ ์ƒ์„ฑ์„ ์œ„ํ•œ ํ—ฌํผ + */ + +export interface SelectOptions { + columns?: string[]; + where?: Record; + joins?: JoinClause[]; + orderBy?: string; + limit?: number; + offset?: number; + groupBy?: string[]; + having?: Record; +} + +export interface JoinClause { + type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'; + table: string; + on: string; +} + +export interface InsertOptions { + returning?: string[]; + onConflict?: { + columns: string[]; + action: 'DO NOTHING' | 'DO UPDATE'; + updateSet?: string[]; + }; +} + +export interface UpdateOptions { + returning?: string[]; +} + +export interface QueryResult { + query: string; + params: any[]; +} + +export class QueryBuilder { + /** + * SELECT ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static select(table: string, options: SelectOptions = {}): QueryResult { + const { + columns = ['*'], + where = {}, + joins = [], + orderBy, + limit, + offset, + groupBy = [], + having = {}, + } = options; + + let query = `SELECT ${columns.join(', ')} FROM ${table}`; + const params: any[] = []; + let paramIndex = 1; + + // JOIN ์ ˆ ์ถ”๊ฐ€ + for (const join of joins) { + query += ` ${join.type} JOIN ${join.table} ON ${join.on}`; + } + + // WHERE ์ ˆ ์ถ”๊ฐ€ + const whereConditions = Object.keys(where); + if (whereConditions.length > 0) { + const whereClause = whereConditions + .map((key) => { + params.push(where[key]); + return `${key} = $${paramIndex++}`; + }) + .join(' AND '); + query += ` WHERE ${whereClause}`; + } + + // GROUP BY ์ ˆ ์ถ”๊ฐ€ + if (groupBy.length > 0) { + query += ` GROUP BY ${groupBy.join(', ')}`; + } + + // HAVING ์ ˆ ์ถ”๊ฐ€ + const havingConditions = Object.keys(having); + if (havingConditions.length > 0) { + const havingClause = havingConditions + .map((key) => { + params.push(having[key]); + return `${key} = $${paramIndex++}`; + }) + .join(' AND '); + query += ` HAVING ${havingClause}`; + } + + // ORDER BY ์ ˆ ์ถ”๊ฐ€ + if (orderBy) { + query += ` ORDER BY ${orderBy}`; + } + + // LIMIT ์ ˆ ์ถ”๊ฐ€ + if (limit !== undefined) { + params.push(limit); + query += ` LIMIT $${paramIndex++}`; + } + + // OFFSET ์ ˆ ์ถ”๊ฐ€ + if (offset !== undefined) { + params.push(offset); + query += ` OFFSET $${paramIndex++}`; + } + + return { query, params }; + } + + /** + * INSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static insert( + table: string, + data: Record, + options: InsertOptions = {} + ): QueryResult { + const { returning = [], onConflict } = options; + + const columns = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); + + let query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`; + + // ON CONFLICT ์ ˆ ์ถ”๊ฐ€ + if (onConflict) { + query += ` ON CONFLICT (${onConflict.columns.join(', ')})`; + + if (onConflict.action === 'DO NOTHING') { + query += ' DO NOTHING'; + } else if (onConflict.action === 'DO UPDATE' && onConflict.updateSet) { + const updateSet = onConflict.updateSet + .map(col => `${col} = EXCLUDED.${col}`) + .join(', '); + query += ` DO UPDATE SET ${updateSet}`; + } + } + + // RETURNING ์ ˆ ์ถ”๊ฐ€ + if (returning.length > 0) { + query += ` RETURNING ${returning.join(', ')}`; + } + + return { query, params: values }; + } + + /** + * UPDATE ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static update( + table: string, + data: Record, + where: Record, + options: UpdateOptions = {} + ): QueryResult { + const { returning = [] } = options; + + const dataKeys = Object.keys(data); + const dataValues = Object.values(data); + const whereKeys = Object.keys(where); + const whereValues = Object.values(where); + + let paramIndex = 1; + + // SET ์ ˆ ์ƒ์„ฑ + const setClause = dataKeys + .map((key) => `${key} = $${paramIndex++}`) + .join(', '); + + // WHERE ์ ˆ ์ƒ์„ฑ + const whereClause = whereKeys + .map((key) => `${key} = $${paramIndex++}`) + .join(' AND '); + + let query = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`; + + // RETURNING ์ ˆ ์ถ”๊ฐ€ + if (returning.length > 0) { + query += ` RETURNING ${returning.join(', ')}`; + } + + const params = [...dataValues, ...whereValues]; + + return { query, params }; + } + + /** + * DELETE ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static delete(table: string, where: Record): QueryResult { + const whereKeys = Object.keys(where); + const whereValues = Object.values(where); + + const whereClause = whereKeys + .map((key, index) => `${key} = $${index + 1}`) + .join(' AND '); + + const query = `DELETE FROM ${table} WHERE ${whereClause}`; + + return { query, params: whereValues }; + } + + /** + * COUNT ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static count(table: string, where: Record = {}): QueryResult { + const whereKeys = Object.keys(where); + const whereValues = Object.values(where); + + let query = `SELECT COUNT(*) as count FROM ${table}`; + + if (whereKeys.length > 0) { + const whereClause = whereKeys + .map((key, index) => `${key} = $${index + 1}`) + .join(' AND '); + query += ` WHERE ${whereClause}`; + } + + return { query, params: whereValues }; + } + + /** + * EXISTS ์ฟผ๋ฆฌ ์ƒ์„ฑ + */ + static exists(table: string, where: Record): QueryResult { + const whereKeys = Object.keys(where); + const whereValues = Object.values(where); + + const whereClause = whereKeys + .map((key, index) => `${key} = $${index + 1}`) + .join(' AND '); + + const query = `SELECT EXISTS(SELECT 1 FROM ${table} WHERE ${whereClause}) as exists`; + + return { query, params: whereValues }; + } + + /** + * ๋™์  WHERE ์ ˆ ์ƒ์„ฑ (๋ณต์žกํ•œ ์กฐ๊ฑด) + */ + static buildWhereClause( + conditions: Record, + startParamIndex: number = 1 + ): { clause: string; params: any[]; nextParamIndex: number } { + const keys = Object.keys(conditions); + const params: any[] = []; + let paramIndex = startParamIndex; + + if (keys.length === 0) { + return { clause: '', params: [], nextParamIndex: paramIndex }; + } + + const clause = keys + .map((key) => { + const value = conditions[key]; + + // ํŠน์ˆ˜ ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + if (key.includes('>>') || key.includes('->')) { + // JSON ์ฟผ๋ฆฌ + params.push(value); + return `${key} = $${paramIndex++}`; + } else if (Array.isArray(value)) { + // IN ์ ˆ + const placeholders = value.map(() => `$${paramIndex++}`).join(', '); + params.push(...value); + return `${key} IN (${placeholders})`; + } else if (value === null) { + // NULL ์ฒดํฌ + return `${key} IS NULL`; + } else { + // ์ผ๋ฐ˜ ์กฐ๊ฑด + params.push(value); + return `${key} = $${paramIndex++}`; + } + }) + .join(' AND '); + + return { clause, params, nextParamIndex: paramIndex }; + } +}