# ๐Ÿ“‹ Phase 3.16: Data Management Services Raw Query ์ „ํ™˜ ๊ณ„ํš ## ๐Ÿ“‹ ๊ฐœ์š” ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ๊ด€๋ จ ์„œ๋น„์Šค๋“ค์€ ์ด **18๊ฐœ์˜ Prisma ํ˜ธ์ถœ**์ด ์žˆ์œผ๋ฉฐ, ๋™์  ํผ, ๋ฐ์ดํ„ฐ ๋งคํ•‘, ๋ฐ์ดํ„ฐ ์„œ๋น„์Šค, ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ### ๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด | ํ•ญ๋ชฉ | ๋‚ด์šฉ | | --------------- | ----------------------------------------------------- | | ๋Œ€์ƒ ์„œ๋น„์Šค | 4๊ฐœ (EnhancedDynamicForm, DataMapping, Data, Admin) | | ํŒŒ์ผ ์œ„์น˜ | `backend-node/src/services/{enhanced,data,admin}*.ts` | | ์ด ํŒŒ์ผ ํฌ๊ธฐ | 2,062 ๋ผ์ธ | | Prisma ํ˜ธ์ถœ | 0๊ฐœ (์ „ํ™˜ ์™„๋ฃŒ) | | **ํ˜„์žฌ ์ง„ํ–‰๋ฅ ** | **18/18 (100%)** โœ… **์ „ํ™˜ ์™„๋ฃŒ** | | ๋ณต์žก๋„ | ์ค‘๊ฐ„ (๋™์  ์ฟผ๋ฆฌ, JSON ํ•„๋“œ, ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ) | | ์šฐ์„ ์ˆœ์œ„ | ๐ŸŸก ์ค‘๊ฐ„ (Phase 3.16) | | **์ƒํƒœ** | โœ… **์™„๋ฃŒ** | --- ## โœ… ์ „ํ™˜ ์™„๋ฃŒ ๋‚ด์—ญ ### ์ „ํ™˜๋œ Prisma ํ˜ธ์ถœ (18๊ฐœ) #### 1. EnhancedDynamicFormService (6๊ฐœ) - `validateTableExists()` - $queryRawUnsafe โ†’ query - `getTableColumns()` - $queryRawUnsafe โ†’ query - `getColumnWebTypes()` - $queryRawUnsafe โ†’ query - `getPrimaryKeys()` - $queryRawUnsafe โ†’ query - `performInsert()` - $queryRawUnsafe โ†’ query - `performUpdate()` - $queryRawUnsafe โ†’ query #### 2. DataMappingService (5๊ฐœ) - `getSourceData()` - $queryRawUnsafe โ†’ query - `executeInsert()` - $executeRawUnsafe โ†’ query - `executeUpsert()` - $executeRawUnsafe โ†’ query - `executeUpdate()` - $executeRawUnsafe โ†’ query - `disconnect()` - ์ œ๊ฑฐ (Raw Query๋Š” disconnect ๋ถˆํ•„์š”) #### 3. DataService (4๊ฐœ) - `getTableData()` - $queryRawUnsafe โ†’ query - `checkTableExists()` - $queryRawUnsafe โ†’ query - `getTableColumnsSimple()` - $queryRawUnsafe โ†’ query - `getColumnLabel()` - $queryRawUnsafe โ†’ query #### 4. AdminService (3๊ฐœ) - `getAdminMenuList()` - $queryRaw โ†’ query (WITH RECURSIVE) - `getUserMenuList()` - $queryRaw โ†’ query (WITH RECURSIVE) - `getMenuInfo()` - findUnique โ†’ query (JOIN) ### ์ฃผ์š” ๊ธฐ์ˆ ์  ํ•ด๊ฒฐ ์‚ฌํ•ญ 1. **๋ณ€์ˆ˜๋ช… ์ถฉ๋Œ ํ•ด๊ฒฐ** - `dataService.ts`์—์„œ `query` ๋ณ€์ˆ˜ โ†’ `sql` ๋ณ€์ˆ˜๋กœ ๋ณ€๊ฒฝ - `query()` ํ•จ์ˆ˜์™€ ๋กœ์ปฌ ๋ณ€์ˆ˜ ์ถฉ๋Œ ๋ฐฉ์ง€ 2. **WITH RECURSIVE ์ฟผ๋ฆฌ ์ „ํ™˜** - Prisma์˜ `$queryRaw` ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด โ†’ ์ผ๋ฐ˜ ๋ฌธ์ž์—ด - `${userLang}` โ†’ `$1` ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ 3. **JOIN ์ฟผ๋ฆฌ ์ „ํ™˜** - Prisma์˜ `include` ์˜ต์…˜ โ†’ `LEFT JOIN` ์ฟผ๋ฆฌ - ๊ด€๊ณ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ์กฐํšŒ 4. **๋™์  ์ฟผ๋ฆฌ ์ƒ์„ฑ** - ๋™์  WHERE ์กฐ๊ฑด ๊ตฌ์„ฑ - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ (์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ) - ๋™์  ORDER BY ์ฒ˜๋ฆฌ ### ์ปดํŒŒ์ผ ์ƒํƒœ โœ… TypeScript ์ปดํŒŒ์ผ ์„ฑ๊ณต โœ… Linter ์˜ค๋ฅ˜ ์—†์Œ --- ## ๐Ÿ” ์„œ๋น„์Šค๋ณ„ ์ƒ์„ธ ๋ถ„์„ ### 1. EnhancedDynamicFormService (6๊ฐœ ํ˜ธ์ถœ, 786 ๋ผ์ธ) **์ฃผ์š” ๊ธฐ๋Šฅ**: - ๊ณ ๊ธ‰ ๋™์  ํผ ๊ด€๋ฆฌ - ํผ ๊ฒ€์ฆ ๊ทœ์น™ - ์กฐ๊ฑด๋ถ€ ํ•„๋“œ ํ‘œ์‹œ - ํผ ํ…œํ”Œ๋ฆฟ ๊ด€๋ฆฌ **์˜ˆ์ƒ Prisma ํ˜ธ์ถœ**: - `getEnhancedForms()` - ๊ณ ๊ธ‰ ํผ ๋ชฉ๋ก ์กฐํšŒ - `getEnhancedForm()` - ๊ณ ๊ธ‰ ํผ ๋‹จ๊ฑด ์กฐํšŒ - `createEnhancedForm()` - ๊ณ ๊ธ‰ ํผ ์ƒ์„ฑ - `updateEnhancedForm()` - ๊ณ ๊ธ‰ ํผ ์ˆ˜์ • - `deleteEnhancedForm()` - ๊ณ ๊ธ‰ ํผ ์‚ญ์ œ - `getFormValidationRules()` - ๊ฒ€์ฆ ๊ทœ์น™ ์กฐํšŒ **๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ**: - JSON ํ•„๋“œ (validation_rules, conditional_logic, field_config) - ๋ณต์žกํ•œ ๊ฒ€์ฆ ๊ทœ์น™ - ๋™์  ํ•„๋“œ ์ƒ์„ฑ - ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ๋กœ์ง --- ### 2. DataMappingService (5๊ฐœ ํ˜ธ์ถœ, 575 ๋ผ์ธ) **์ฃผ์š” ๊ธฐ๋Šฅ**: - ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์„ค์ • ๊ด€๋ฆฌ - ์†Œ์Šค-ํƒ€๊ฒŸ ํ•„๋“œ ๋งคํ•‘ - ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๊ทœ์น™ - ๋งคํ•‘ ์‹คํ–‰ **์˜ˆ์ƒ Prisma ํ˜ธ์ถœ**: - `getDataMappings()` - ๋งคํ•‘ ์„ค์ • ๋ชฉ๋ก ์กฐํšŒ - `getDataMapping()` - ๋งคํ•‘ ์„ค์ • ๋‹จ๊ฑด ์กฐํšŒ - `createDataMapping()` - ๋งคํ•‘ ์„ค์ • ์ƒ์„ฑ - `updateDataMapping()` - ๋งคํ•‘ ์„ค์ • ์ˆ˜์ • - `deleteDataMapping()` - ๋งคํ•‘ ์„ค์ • ์‚ญ์ œ **๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ**: - JSON ํ•„๋“œ (field_mappings, transformation_rules) - ๋ณต์žกํ•œ ๋ณ€ํ™˜ ๋กœ์ง - ๋งคํ•‘ ๊ฒ€์ฆ - ์‹คํ–‰ ์ด๋ ฅ ์ถ”์  --- ### 3. DataService (4๊ฐœ ํ˜ธ์ถœ, 327 ๋ผ์ธ) **์ฃผ์š” ๊ธฐ๋Šฅ**: - ๋™์  ๋ฐ์ดํ„ฐ ์กฐํšŒ - ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง - ๋ฐ์ดํ„ฐ ์ •๋ ฌ - ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ **์˜ˆ์ƒ Prisma ํ˜ธ์ถœ**: - `getDataByTable()` - ํ…Œ์ด๋ธ”๋ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ - `getDataById()` - ๋ฐ์ดํ„ฐ ๋‹จ๊ฑด ์กฐํšŒ - `executeCustomQuery()` - ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ์‹คํ–‰ - `getDataStatistics()` - ๋ฐ์ดํ„ฐ ํ†ต๊ณ„ ์กฐํšŒ **๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ**: - ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ - ๋™์  WHERE ์กฐ๊ฑด - ์ง‘๊ณ„ ์ฟผ๋ฆฌ --- ### 4. AdminService (3๊ฐœ ํ˜ธ์ถœ, 374 ๋ผ์ธ) **์ฃผ์š” ๊ธฐ๋Šฅ**: - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ๊ด€๋ฆฌ - ์‹œ์Šคํ…œ ์„ค์ • - ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ - ๋กœ๊ทธ ์กฐํšŒ **์˜ˆ์ƒ Prisma ํ˜ธ์ถœ**: - `getAdminMenus()` - ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ์กฐํšŒ - `getSystemSettings()` - ์‹œ์Šคํ…œ ์„ค์ • ์กฐํšŒ - `updateSystemSettings()` - ์‹œ์Šคํ…œ ์„ค์ • ์—…๋ฐ์ดํŠธ **๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ**: - ๋ฉ”๋‰ด ๊ณ„์ธต ๊ตฌ์กฐ - ๊ถŒํ•œ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง - JSON ์„ค์ • ํ•„๋“œ - ์บ์‹ฑ --- ## ๐Ÿ’ก ํ†ตํ•ฉ ์ „ํ™˜ ์ „๋žต ### Phase 1: ๋‹จ์ˆœ CRUD ์ „ํ™˜ (12๊ฐœ) **EnhancedDynamicFormService (6๊ฐœ) + DataMappingService (5๊ฐœ) + AdminService (1๊ฐœ)** - ๊ธฐ๋ณธ CRUD ๊ธฐ๋Šฅ - JSON ํ•„๋“œ ์ฒ˜๋ฆฌ ### Phase 2: ๋™์  ์ฟผ๋ฆฌ ์ „ํ™˜ (4๊ฐœ) **DataService (4๊ฐœ)** - ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ - ๋ณด์•ˆ ๊ฒ€์ฆ ### Phase 3: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ „ํ™˜ (2๊ฐœ) **AdminService (2๊ฐœ)** - ์‹œ์Šคํ…œ ์„ค์ • - ์บ์‹ฑ --- ## ๐Ÿ’ป ์ „ํ™˜ ์˜ˆ์‹œ ### ์˜ˆ์‹œ 1: ๊ณ ๊ธ‰ ํผ ์ƒ์„ฑ (JSON ํ•„๋“œ) **๋ณ€๊ฒฝ ์ „**: ```typescript const form = await prisma.enhanced_forms.create({ data: { form_code: formCode, form_name: formName, validation_rules: validationRules, // JSON conditional_logic: conditionalLogic, // JSON field_config: fieldConfig, // JSON company_code: companyCode, }, }); ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript const form = await queryOne( `INSERT INTO enhanced_forms (form_code, form_name, validation_rules, conditional_logic, field_config, company_code, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) RETURNING *`, [ formCode, formName, JSON.stringify(validationRules), JSON.stringify(conditionalLogic), JSON.stringify(fieldConfig), companyCode, ] ); ``` ### ์˜ˆ์‹œ 2: ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์กฐํšŒ **๋ณ€๊ฒฝ ์ „**: ```typescript const mappings = await prisma.data_mappings.findMany({ where: { source_table: sourceTable, target_table: targetTable, is_active: true, }, include: { source_columns: true, target_columns: true, }, }); ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript const mappings = await query( `SELECT dm.*, json_agg(DISTINCT jsonb_build_object( 'column_id', sc.column_id, 'column_name', sc.column_name )) FILTER (WHERE sc.column_id IS NOT NULL) as source_columns, json_agg(DISTINCT jsonb_build_object( 'column_id', tc.column_id, 'column_name', tc.column_name )) FILTER (WHERE tc.column_id IS NOT NULL) as target_columns FROM data_mappings dm LEFT JOIN columns sc ON dm.mapping_id = sc.mapping_id AND sc.type = 'source' LEFT JOIN columns tc ON dm.mapping_id = tc.mapping_id AND tc.type = 'target' WHERE dm.source_table = $1 AND dm.target_table = $2 AND dm.is_active = $3 GROUP BY dm.mapping_id`, [sourceTable, targetTable, true] ); ``` ### ์˜ˆ์‹œ 3: ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ (DataService) **๋ณ€๊ฒฝ ์ „**: ```typescript // Prisma๋กœ๋Š” ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ // ์ด๋ฏธ $queryRawUnsafe ์‚ฌ์šฉ ์ค‘์ผ ๊ฐ€๋Šฅ์„ฑ const data = await prisma.$queryRawUnsafe( `SELECT * FROM ${tableName} WHERE ${whereClause}`, ...params ); ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript // SQL ์ธ์ ์…˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ const validTableName = validateTableName(tableName); const data = await query( `SELECT * FROM ${validTableName} WHERE ${whereClause}`, params ); ``` ### ์˜ˆ์‹œ 4: ๊ด€๋ฆฌ์ž ๋ฉ”๋‰ด ์กฐํšŒ (๊ณ„์ธต ๊ตฌ์กฐ) **๋ณ€๊ฒฝ ์ „**: ```typescript const menus = await prisma.admin_menus.findMany({ where: { is_active: true }, orderBy: { sort_order: "asc" }, include: { children: { orderBy: { sort_order: "asc" }, }, }, }); ``` **๋ณ€๊ฒฝ ํ›„**: ```typescript // ์žฌ๊ท€ CTE๋ฅผ ์‚ฌ์šฉํ•œ ๊ณ„์ธต ์ฟผ๋ฆฌ const menus = await query( `WITH RECURSIVE menu_tree AS ( SELECT *, 0 as level, ARRAY[menu_id] as path FROM admin_menus WHERE parent_id IS NULL AND is_active = $1 UNION ALL SELECT m.*, mt.level + 1, mt.path || m.menu_id FROM admin_menus m JOIN menu_tree mt ON m.parent_id = mt.menu_id WHERE m.is_active = $1 ) SELECT * FROM menu_tree ORDER BY path, sort_order`, [true] ); ``` --- ## ๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ ### 1. JSON ํ•„๋“œ ์ฒ˜๋ฆฌ ```typescript // ๋ณต์žกํ•œ JSON ๊ตฌ์กฐ interface ValidationRules { required?: string[]; min?: Record; max?: Record; pattern?: Record; custom?: Array<{ field: string; rule: string }>; } // ์ €์žฅ ์‹œ JSON.stringify(validationRules); // ์กฐํšŒ ํ›„ const parsed = typeof row.validation_rules === "string" ? JSON.parse(row.validation_rules) : row.validation_rules; ``` ### 2. ๋™์  ํ…Œ์ด๋ธ” ์ฟผ๋ฆฌ ๋ณด์•ˆ ```typescript // ํ…Œ์ด๋ธ”๋ช… ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ const ALLOWED_TABLES = ["users", "products", "orders"]; function validateTableName(tableName: string): string { if (!ALLOWED_TABLES.includes(tableName)) { throw new Error("Invalid table name"); } return tableName; } // ์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ function validateColumnName(columnName: string): string { if (!/^[a-z_][a-z0-9_]*$/i.test(columnName)) { throw new Error("Invalid column name"); } return columnName; } ``` ### 3. ์žฌ๊ท€ CTE (๊ณ„์ธต ๊ตฌ์กฐ) ```sql WITH RECURSIVE hierarchy AS ( -- ์ตœ์ƒ์œ„ ๋…ธ๋“œ SELECT * FROM table WHERE parent_id IS NULL UNION ALL -- ํ•˜์œ„ ๋…ธ๋“œ SELECT t.* FROM table t JOIN hierarchy h ON t.parent_id = h.id ) SELECT * FROM hierarchy ``` ### 4. JSON ์ง‘๊ณ„ (๊ด€๊ณ„ ๋ฐ์ดํ„ฐ) ```sql SELECT parent.*, COALESCE( json_agg( jsonb_build_object('id', child.id, 'name', child.name) ) FILTER (WHERE child.id IS NOT NULL), '[]' ) as children FROM parent LEFT JOIN child ON parent.id = child.parent_id GROUP BY parent.id ``` --- ## ๐Ÿ“ ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ### EnhancedDynamicFormService (6๊ฐœ) - [ ] `getEnhancedForms()` - ๋ชฉ๋ก ์กฐํšŒ - [ ] `getEnhancedForm()` - ๋‹จ๊ฑด ์กฐํšŒ - [ ] `createEnhancedForm()` - ์ƒ์„ฑ (JSON ํ•„๋“œ) - [ ] `updateEnhancedForm()` - ์ˆ˜์ • (JSON ํ•„๋“œ) - [ ] `deleteEnhancedForm()` - ์‚ญ์ œ - [ ] `getFormValidationRules()` - ๊ฒ€์ฆ ๊ทœ์น™ ์กฐํšŒ ### DataMappingService (5๊ฐœ) - [ ] `getDataMappings()` - ๋ชฉ๋ก ์กฐํšŒ - [ ] `getDataMapping()` - ๋‹จ๊ฑด ์กฐํšŒ - [ ] `createDataMapping()` - ์ƒ์„ฑ - [ ] `updateDataMapping()` - ์ˆ˜์ • - [ ] `deleteDataMapping()` - ์‚ญ์ œ ### DataService (4๊ฐœ) - [ ] `getDataByTable()` - ๋™์  ํ…Œ์ด๋ธ” ์กฐํšŒ - [ ] `getDataById()` - ๋‹จ๊ฑด ์กฐํšŒ - [ ] `executeCustomQuery()` - ์ปค์Šคํ…€ ์ฟผ๋ฆฌ - [ ] `getDataStatistics()` - ํ†ต๊ณ„ ์กฐํšŒ ### AdminService (3๊ฐœ) - [ ] `getAdminMenus()` - ๋ฉ”๋‰ด ์กฐํšŒ (์žฌ๊ท€ CTE) - [ ] `getSystemSettings()` - ์‹œ์Šคํ…œ ์„ค์ • ์กฐํšŒ - [ ] `updateSystemSettings()` - ์‹œ์Šคํ…œ ์„ค์ • ์—…๋ฐ์ดํŠธ ### ๊ณตํ†ต ์ž‘์—… - [ ] import ๋ฌธ ์ˆ˜์ • (๋ชจ๋“  ์„œ๋น„์Šค) - [ ] Prisma import ์™„์ „ ์ œ๊ฑฐ - [ ] JSON ํ•„๋“œ ์ฒ˜๋ฆฌ ํ™•์ธ - [ ] ๋ณด์•ˆ ๊ฒ€์ฆ (SQL ์ธ์ ์…˜) --- ## ๐Ÿงช ํ…Œ์ŠคํŠธ ๊ณ„ํš ### ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (18๊ฐœ) - ๊ฐ Prisma ํ˜ธ์ถœ๋ณ„ 1๊ฐœ์”ฉ ### ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (6๊ฐœ) - EnhancedDynamicFormService: ํผ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ (2๊ฐœ) - DataMappingService: ๋งคํ•‘ ์„ค์ • ๋ฐ ์‹คํ–‰ ํ…Œ์ŠคํŠธ (2๊ฐœ) - DataService: ๋™์  ์ฟผ๋ฆฌ ๋ฐ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ (1๊ฐœ) - AdminService: ๋ฉ”๋‰ด ๊ณ„์ธต ๊ตฌ์กฐ ํ…Œ์ŠคํŠธ (1๊ฐœ) ### ๋ณด์•ˆ ํ…Œ์ŠคํŠธ - SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ํ…Œ์ŠคํŠธ - ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ - ์ปฌ๋Ÿผ๋ช… ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ --- ## ๐ŸŽฏ ์˜ˆ์ƒ ๋‚œ์ด๋„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ - **๋‚œ์ด๋„**: โญโญโญโญ (๋†’์Œ) - JSON ํ•„๋“œ ์ฒ˜๋ฆฌ - ๋™์  ์ฟผ๋ฆฌ ๋ณด์•ˆ - ์žฌ๊ท€ CTE - JSON ์ง‘๊ณ„ - **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 2.5~3์‹œ๊ฐ„ - Phase 1 (๊ธฐ๋ณธ CRUD): 1์‹œ๊ฐ„ - Phase 2 (๋™์  ์ฟผ๋ฆฌ): 1์‹œ๊ฐ„ - Phase 3 (๊ณ ๊ธ‰ ๊ธฐ๋Šฅ): 0.5์‹œ๊ฐ„ - ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™”: 0.5์‹œ๊ฐ„ --- ## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ### ๋ณด์•ˆ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ 1. โœ… ๋™์  ํ…Œ์ด๋ธ”๋ช…์€ ๋ฐ˜๋“œ์‹œ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ 2. โœ… ๋™์  ์ปฌ๋Ÿผ๋ช…์€ ์ •๊ทœ์‹์œผ๋กœ ๊ฒ€์ฆ 3. โœ… WHERE ์ ˆ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ฐ˜๋“œ์‹œ ๋ฐ”์ธ๋”ฉ 4. โœ… JSON ํ•„๋“œ๋Š” ํŒŒ์‹ฑ ์—๋Ÿฌ ์ฒ˜๋ฆฌ 5. โœ… ์žฌ๊ท€ ์ฟผ๋ฆฌ๋Š” ๊นŠ์ด ์ œํ•œ ์„ค์ • ### ์„ฑ๋Šฅ ์ตœ์ ํ™” - JSON ํ•„๋“œ ์ธ๋ฑ์‹ฑ (GIN ์ธ๋ฑ์Šค) - ์žฌ๊ท€ ์ฟผ๋ฆฌ ๊นŠ์ด ์ œํ•œ - ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์ตœ์ ํ™” - ํ•„์š”์‹œ ์บ์‹ฑ ์ ์šฉ --- **์ƒํƒœ**: โณ **๋Œ€๊ธฐ ์ค‘** **ํŠน์ด์‚ฌํ•ญ**: JSON ํ•„๋“œ, ๋™์  ์ฟผ๋ฆฌ, ์žฌ๊ท€ CTE, ๋ณด์•ˆ ๊ฒ€์ฆ ํฌํ•จ **โš ๏ธ ์ฃผ์˜**: ๋™์  ์ฟผ๋ฆฌ๋Š” SQL ์ธ์ ์…˜ ๋ฐฉ์ง€๊ฐ€ ๋งค์šฐ ์ค‘์š”!