diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index d4e8d0cf..a648a4f9 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -793,8 +793,9 @@ export const previewFile = async ( return; } - // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ) - if (companyCode !== "*" && fileRecord.company_code !== companyCode) { + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ๋ฐ ๊ณต๊ฐœ ์ ‘๊ทผ ์ œ์™ธ) + // ๊ณต๊ฐœ ์ ‘๊ทผ(req.user๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ)์€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ—ˆ์šฉ (์ด๋ฏธ์ง€ ํ‘œ์‹œ์šฉ) + if (companyCode && companyCode !== "*" && fileRecord.company_code !== companyCode) { console.warn("โš ๏ธ ๋‹ค๋ฅธ ํšŒ์‚ฌ ํŒŒ์ผ ์ ‘๊ทผ ์‹œ๋„:", { userId: req.user?.userId, userCompanyCode: companyCode, diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index df0c4f4d..32ce60c3 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -46,11 +46,13 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) const countResult = await pool.query(countQuery, params); const total = parseInt(countResult.rows[0].total); - // ๋ฐ์ดํ„ฐ ์กฐํšŒ (screens ๋ฐฐ์—ด ํฌํ•จ) + // ๋ฐ์ดํ„ฐ ์กฐํšŒ (screens ๋ฐฐ์—ด ํฌํ•จ) - ์‚ญ์ œ๋œ ํ™”๋ฉด(is_active = 'D') ์ œ์™ธ const dataQuery = ` SELECT sg.*, - (SELECT COUNT(*) FROM screen_group_screens sgs WHERE sgs.group_id = sg.id) as screen_count, + (SELECT COUNT(*) FROM screen_group_screens sgs + LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id + WHERE sgs.group_id = sg.id AND sd.is_active != 'D') as screen_count, (SELECT json_agg( json_build_object( 'id', sgs.id, @@ -64,6 +66,7 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) ) FROM screen_group_screens sgs LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id WHERE sgs.group_id = sg.id + AND sd.is_active != 'D' ) as screens FROM screen_groups sg ${whereClause} @@ -111,6 +114,7 @@ export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) = ) FROM screen_group_screens sgs LEFT JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id WHERE sgs.group_id = sg.id + AND sd.is_active != 'D' ) as screens FROM screen_groups sg WHERE sg.id = $1 @@ -1737,7 +1741,9 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons }); // 4. rightPanel.relation ํŒŒ์‹ฑ (split-panel-layout ๋“ฑ์—์„œ ์‚ฌ์šฉ) + // screen_layouts (v1)์™€ screen_layouts_v2 ๋ชจ๋‘ ์กฐํšŒ const rightPanelQuery = ` + -- V1: screen_layouts์—์„œ ์กฐํšŒ SELECT sd.screen_id, sd.screen_name, @@ -1750,6 +1756,23 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sd.screen_id = ANY($1) AND sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL + + UNION ALL + + -- V2: screen_layouts_v2์—์„œ ์กฐํšŒ (v2-split-panel-layout ์ปดํฌ๋„ŒํŠธ) + SELECT + sd.screen_id, + sd.screen_name, + sd.table_name as main_table, + comp->'overrides'->>'type' as component_type, + comp->'overrides'->'rightPanel'->'relation' as right_panel_relation, + comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table, + comp->'overrides'->'rightPanel'->'columns' as right_panel_columns + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp + WHERE sd.screen_id = ANY($1) + AND comp->'overrides'->'rightPanel'->'relation' IS NOT NULL `; const rightPanelResult = await pool.query(rightPanelQuery, [screenIds]); @@ -2118,9 +2141,56 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons })) }); + // ============================================================ + // 6. ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ˆ˜์ง‘ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ์šฉ) + // ============================================================ + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // 1. screen_definitions.table_name (์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ) + // 2. v2-split-panel-layout์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + // + // ์ด ๋ชฉ๋ก์— ์žˆ์œผ๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + const globalMainTablesQuery = ` + -- 1. ๋ชจ๋“  ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ” (screen_definitions.table_name) + SELECT DISTINCT table_name as main_table + FROM screen_definitions + WHERE screen_id = ANY($1) + AND table_name IS NOT NULL + + UNION + + -- 2. v2-split-panel-layout์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + -- ํ˜„์žฌ ๊ทธ๋ฃน์˜ ํ™”๋ฉด๋“ค์—์„œ ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ๋กœ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” + SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table + FROM screen_definitions sd + JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, + jsonb_array_elements(slv2.layout_data->'components') as comp + WHERE sd.screen_id = ANY($1) + AND comp->'overrides'->'rightPanel'->>'tableName' IS NOT NULL + + UNION + + -- 3. v1 screen_layouts์˜ rightPanel.tableName (WHERE ์กฐ๊ฑด ๋Œ€์ƒ) + SELECT DISTINCT sl.properties->'componentConfig'->'rightPanel'->>'tableName' as main_table + FROM screen_definitions sd + JOIN screen_layouts sl ON sd.screen_id = sl.screen_id + WHERE sd.screen_id = ANY($1) + AND sl.properties->'componentConfig'->'rightPanel'->>'tableName' IS NOT NULL + `; + + const globalMainTablesResult = await pool.query(globalMainTablesQuery, [screenIds]); + const globalMainTables = globalMainTablesResult.rows + .map((r: any) => r.main_table) + .filter((t: string) => t != null && t !== ''); + + logger.info("์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ˆ˜์ง‘ ์™„๋ฃŒ", { + count: globalMainTables.length, + tables: globalMainTables + }); + res.json({ success: true, data: screenSubTables, + globalMainTables: globalMainTables, // ๋ฉ”์ธ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์–ด์•ผ ํ•˜๋Š” ํ…Œ์ด๋ธ” ๋ชฉ๋ก }); } catch (error: any) { logger.error("ํ™”๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ ์‹คํŒจ:", error); diff --git a/backend-node/src/routes/fileRoutes.ts b/backend-node/src/routes/fileRoutes.ts index 64f02d14..4514e37f 100644 --- a/backend-node/src/routes/fileRoutes.ts +++ b/backend-node/src/routes/fileRoutes.ts @@ -24,6 +24,13 @@ const router = Router(); */ router.get("/public/:token", getFileByToken); +/** + * @route GET /api/files/preview/:objid + * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - ๊ณต๊ฐœ ์ ‘๊ทผ ํ—ˆ์šฉ + * @access Public + */ +router.get("/preview/:objid", previewFile); + // ๋ชจ๋“  ํŒŒ์ผ API๋Š” ์ธ์ฆ ํ•„์š” router.use(authenticateToken); @@ -64,12 +71,7 @@ router.get("/linked/:tableName/:recordId", getLinkedFiles); */ router.delete("/:objid", deleteFile); -/** - * @route GET /api/files/preview/:objid - * @desc ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ด๋ฏธ์ง€ ๋“ฑ) - * @access Private - */ -router.get("/preview/:objid", previewFile); +// preview ๋ผ์šฐํŠธ๋Š” ์ƒ๋‹จ ๊ณต๊ฐœ ์ ‘๊ทผ ๊ตฌ์—ญ์œผ๋กœ ์ด๋™๋จ /** * @route GET /api/files/download/:objid diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 572f2443..37a21a0a 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -731,6 +731,14 @@ export class ScreenManagementService { WHERE screen_id = $1 AND is_active = 'Y'`, [screenId], ); + + // 5. ํ™”๋ฉด ๊ทธ๋ฃน ์—ฐ๊ฒฐ ์‚ญ์ œ (screen_group_screens) + await client.query( + `DELETE FROM screen_group_screens WHERE screen_id = $1`, + [screenId], + ); + + logger.info("ํ™”๋ฉด ์‚ญ์ œ ์‹œ ๊ทธ๋ฃน ์—ฐ๊ฒฐ ํ•ด์ œ", { screenId }); }); } @@ -5110,18 +5118,6 @@ export class ScreenManagementService { console.log( `V2 ๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์™„๋ฃŒ: ${layout.layout_data?.components?.length || 0}๊ฐœ ์ปดํฌ๋„ŒํŠธ`, ); - - // ๐Ÿ› ๋””๋ฒ„๊น…: finished_timeline์˜ fieldMapping ํ™•์ธ - const splitPanel = layout.layout_data?.components?.find((c: any) => - c.url?.includes("v2-split-panel-layout") - ); - const finishedTimeline = splitPanel?.overrides?.rightPanel?.components?.find( - (c: any) => c.id === "finished_timeline" - ); - if (finishedTimeline) { - console.log("๐Ÿ› [Backend] finished_timeline fieldMapping:", JSON.stringify(finishedTimeline.componentConfig?.fieldMapping)); - } - return layout.layout_data; } @@ -5161,20 +5157,16 @@ export class ScreenManagementService { ...layoutData }; - // SUPER_ADMIN์ธ ๊ฒฝ์šฐ ํ™”๋ฉด ์ •์˜์˜ company_code๋กœ ์ €์žฅ (๋กœ๋“œ์™€ ์ผ๊ด€์„ฑ ์œ ์ง€) - const saveCompanyCode = companyCode === "*" ? existingScreen.company_code : companyCode; - console.log(`์ €์žฅํ•  company_code: ${saveCompanyCode} (์›๋ณธ: ${companyCode}, ํ™”๋ฉด ์ •์˜: ${existingScreen.company_code})`); - // UPSERT (์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ, ์—†์œผ๋ฉด ์‚ฝ์ž…) await query( `INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (screen_id, company_code) DO UPDATE SET layout_data = $3, updated_at = NOW()`, - [screenId, saveCompanyCode, JSON.stringify(dataToSave)], + [screenId, companyCode, JSON.stringify(dataToSave)], ); - console.log(`V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ ์™„๋ฃŒ (company_code: ${saveCompanyCode})`); + console.log(`V2 ๋ ˆ์ด์•„์›ƒ ์ €์žฅ ์™„๋ฃŒ`); } } diff --git a/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md b/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md new file mode 100644 index 00000000..f885debb --- /dev/null +++ b/docs/DDD1542/FLOW_BASED_RESPONSIVE_DESIGN.md @@ -0,0 +1,729 @@ +# Flow ๊ธฐ๋ฐ˜ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ์„ค๊ณ„์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-30 +> ๋ชฉํ‘œ: ์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ (PC/ํƒœ๋ธ”๋ฆฟ/๋ชจ๋ฐ”์ผ ์ „์ฒด ๋Œ€์‘) + +--- + +## 1. ํ•ต์‹ฌ ๊ฒฐ๋ก  + +### 1.1 ํ˜„์žฌ ๋ฐฉ์‹ vs ๋ฐ˜์‘ํ˜• ํ‘œ์ค€ + +| ํ•ญ๋ชฉ | ํ˜„์žฌ ์‹œ์Šคํ…œ | ์›น ํ‘œ์ค€ (2025) | +|------|-------------|----------------| +| ๋ฐฐ์น˜ ๋ฐฉ์‹ | `position: absolute` | **Flexbox / CSS Grid** | +| ์ขŒํ‘œ | ํ”ฝ์…€ ๊ณ ์ • (x, y) | **Flow ๊ธฐ๋ฐ˜ (์ˆœ์„œ)** | +| ํ™”๋ฉด ์ถ•์†Œ ์‹œ | ๊ทธ๋Œ€๋กœ (์ž˜๋ฆผ) | **์ž๋™ ์žฌ๋ฐฐ์น˜** | +| ์šฉ๋„ | ํˆดํŒ, ์˜ค๋ฒ„๋ ˆ์ด | **์ „์ฒด ๋ ˆ์ด์•„์›ƒ** | + +> **๊ฒฐ๋ก **: `position: absolute`๋Š” ์ „์ฒด ๋ ˆ์ด์•„์›ƒ์— ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋จ (์›น ํ‘œ์ค€) + +### 1.2 ๊ตฌํ˜„ ๋ฐฉํ–ฅ + +``` +์ ˆ๋Œ€ ์ขŒํ‘œ (x, y ํ”ฝ์…€) + โ†“ ๋ณ€ํ™˜ +Flow ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ (Flexbox + Grid) + โ†“ ๊ฒฐ๊ณผ +ํ™”๋ฉด ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ž๋™ ์žฌ๋ฐฐ์น˜ +``` + +--- + +## 2. ์‹ค์ œ ํ™”๋ฉด ๋ฐ์ดํ„ฐ ๋ถ„์„ + +### 2.1 ๋ถ„์„ ๋Œ€์ƒ + +``` +์ด ๋ ˆ์ด์•„์›ƒ: 1,250๊ฐœ +์ด ์ปดํฌ๋„ŒํŠธ: 5,236๊ฐœ +๋ถ„์„ ์ƒ˜ํ”Œ: 6๊ฐœ ํ™”๋ฉด (23, 20, 18, 16, 18, 5๊ฐœ ์ปดํฌ๋„ŒํŠธ) +``` + +### 2.2 ํ™”๋ฉด 68 (์ˆ˜์ฃผ ๋ชฉ๋ก) - ๊ฐ€๋กœ ๋ฐฐ์น˜ ํŒจํ„ด + +``` +y=88: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ™์€ ํ–‰์— ๋ฒ„ํŠผ 4๊ฐœ + x=1277 x=1436 x=1594 x=1753 + +y=128: [โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ํ…Œ์ด๋ธ” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€] + x=8, width=1904 +``` + +**๋ณ€ํ™˜ ํ›„**: +```html +
+ + + + +
+
+ + +``` + +**๋ฐ˜์‘ํ˜• ๋™์ž‘**: +``` +1920px: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ€๋กœ ๋ฐฐ์น˜ +1280px: [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ† ๊ฐ€๋กœ ๋ฐฐ์น˜ (๊ณต๊ฐ„ ์ถฉ๋ถ„) + 768px: [๋ถ„๋ฆฌ] [์ €์žฅ] โ† ์ค„๋ฐ”๊ฟˆ ๋ฐœ์ƒ + [์ˆ˜์ •] [์‚ญ์ œ] + 375px: [๋ถ„๋ฆฌ] โ† ์„ธ๋กœ ๋ฐฐ์น˜ + [์ €์žฅ] + [์ˆ˜์ •] + [์‚ญ์ œ] +``` + +### 2.3 ํ™”๋ฉด 119 (์žฅ์น˜ ๊ด€๋ฆฌ) - 2์—ด ํผ ํŒจํ„ด + +``` +y=80: [์žฅ์น˜ ์ฝ”๋“œ ] [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] + x=136, w=256 x=408, w=256 + +y=160: [์ œ์กฐ์‚ฌ ] + x=136, w=528 + +y=240: [ํ’ˆ๋ฒˆ ] [๋ชจ๋ธ๋ช… ] + x=136, w=256 x=408, w=256 + +y=320: [๊ตฌ๋งค์ผ ] [์ƒํƒœ ] +y=400: [๊ณต๊ธ‰์‚ฌ ] [๊ตฌ๋งค ๊ฐ€๊ฒฉ ] +y=480: [๊ณ„์•ฝ ๋ฒˆํ˜ธ ] [๊ณต๊ธ‰์‚ฌ ์ „ํ™” ] +... (2์—ด ๋ฐ˜๋ณต) + +y=840: [์ €์žฅ] + x=544 +``` + +**๋ณ€ํ™˜ ํ›„**: +```html +
+ + +
+
+ +
+
+ + + + +
+ + + +
+ + + +
+
+ + + +
+ +
+``` + +**๋ฐ˜์‘ํ˜• ๋™์ž‘**: +``` +1920px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] [๋‹จ๊ฐ€๋ฐฉ์‹] [๋‹จ๊ฐ€์ˆ˜์ •] โ† 4์—ด +1280px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] [๋‹จ๊ฐ€๋ฐฉ์‹] โ† 3์—ด + [๋‹จ๊ฐ€์ˆ˜์ •] + 768px: [์ž…๋ ฅ๋ฐฉ์‹] [ํŒ๋งค์œ ํ˜•] โ† 2์—ด + [๋‹จ๊ฐ€๋ฐฉ์‹] [๋‹จ๊ฐ€์ˆ˜์ •] + 375px: [์ž…๋ ฅ๋ฐฉ์‹] โ† 1์—ด + [ํŒ๋งค์œ ํ˜•] + [๋‹จ๊ฐ€๋ฐฉ์‹] + [๋‹จ๊ฐ€์ˆ˜์ •] +``` + +--- + +## 3. ๋ณ€ํ™˜ ๊ทœ์น™ + +### 3.1 Row ๊ทธ๋ฃนํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ + +```typescript +const ROW_THRESHOLD = 40; // px + +function groupByRows(components: Component[]): Row[] { + // 1. y ์ขŒํ‘œ๋กœ ์ •๋ ฌ + const sorted = [...components].sort((a, b) => a.position.y - b.position.y); + + const rows: Row[] = []; + let currentRow: Component[] = []; + let currentY = -Infinity; + + for (const comp of sorted) { + if (comp.position.y - currentY > ROW_THRESHOLD) { + // ์ƒˆ๋กœ์šด Row ์‹œ์ž‘ + if (currentRow.length > 0) { + rows.push({ + y: currentY, + components: currentRow.sort((a, b) => a.position.x - b.position.x) + }); + } + currentRow = [comp]; + currentY = comp.position.y; + } else { + // ๊ฐ™์€ Row์— ์ถ”๊ฐ€ + currentRow.push(comp); + } + } + + // ๋งˆ์ง€๋ง‰ Row ์ถ”๊ฐ€ + if (currentRow.length > 0) { + rows.push({ + y: currentY, + components: currentRow.sort((a, b) => a.position.x - b.position.x) + }); + } + + return rows; +} +``` + +### 3.2 ํ™”๋ฉด 68 ์ ์šฉ ์˜ˆ์‹œ + +**์ž…๋ ฅ**: +```json +[ + { "id": "comp_1899", "position": { "x": 1277, "y": 88 }, "text": "๋ถ„๋ฆฌ" }, + { "id": "comp_1898", "position": { "x": 1436, "y": 88 }, "text": "์ €์žฅ" }, + { "id": "comp_1897", "position": { "x": 1594, "y": 88 }, "text": "์ˆ˜์ •" }, + { "id": "comp_1896", "position": { "x": 1753, "y": 88 }, "text": "์‚ญ์ œ" }, + { "id": "comp_1895", "position": { "x": 8, "y": 128 }, "type": "table" } +] +``` + +**๋ณ€ํ™˜ ๊ฒฐ๊ณผ**: +```json +{ + "rows": [ + { + "y": 88, + "justify": "end", + "components": ["comp_1899", "comp_1898", "comp_1897", "comp_1896"] + }, + { + "y": 128, + "justify": "start", + "components": ["comp_1895"] + } + ] +} +``` + +### 3.3 ์ •๋ ฌ ๋ฐฉํ–ฅ ๊ฒฐ์ • + +```typescript +function determineJustify(row: Row, screenWidth: number): string { + const firstX = row.components[0].position.x; + const lastComp = row.components[row.components.length - 1]; + const lastEnd = lastComp.position.x + lastComp.size.width; + + // ์™ผ์ชฝ ์—ฌ๋ฐฑ vs ์˜ค๋ฅธ์ชฝ ์—ฌ๋ฐฑ ๋น„๊ต + const leftMargin = firstX; + const rightMargin = screenWidth - lastEnd; + + if (leftMargin > rightMargin * 2) { + return "end"; // ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ + } else if (rightMargin > leftMargin * 2) { + return "start"; // ์™ผ์ชฝ ์ •๋ ฌ + } else { + return "center"; // ์ค‘์•™ ์ •๋ ฌ + } +} + +// ํ™”๋ฉด 68 ๋ฒ„ํŠผ ๊ทธ๋ฃน: +// leftMargin = 1277, rightMargin = 1920 - 1912 = 8 +// โ†’ "end" (์˜ค๋ฅธ์ชฝ ์ •๋ ฌ) +``` + +--- + +## 4. ๋ Œ๋”๋ง ๊ตฌํ˜„ + +### 4.1 ์ƒˆ๋กœ์šด FlowLayout ์ปดํฌ๋„ŒํŠธ + +```tsx +// frontend/lib/registry/layouts/flow/FlowLayout.tsx + +interface FlowLayoutProps { + layout: LayoutData; + renderer: DynamicComponentRenderer; +} + +export function FlowLayout({ layout, renderer }: FlowLayoutProps) { + // 1. Row ๊ทธ๋ฃนํ™” + const rows = useMemo(() => { + return groupByRows(layout.components); + }, [layout.components]); + + return ( +
+ {rows.map((row, index) => ( + + ))} +
+ ); +} + +function FlowRow({ row, renderer }: { row: Row; renderer: any }) { + const justify = determineJustify(row, 1920); + + const justifyClass = { + start: "justify-start", + center: "justify-center", + end: "justify-end", + }[justify]; + + return ( +
+ {row.components.map((comp) => ( +
+ {renderer.renderChild(comp)} +
+ ))} +
+ ); +} +``` + +### 4.2 ๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ • ์œ„์น˜ + +**ํ˜„์žฌ (RealtimePreviewDynamic.tsx ๋ผ์ธ 524-536)**: +```tsx +const baseStyle = { + left: `${adjustedPositionX}px`, // โŒ ์ ˆ๋Œ€ ์ขŒํ‘œ + top: `${position.y}px`, // โŒ ์ ˆ๋Œ€ ์ขŒํ‘œ + position: "absolute", // โŒ ์ ˆ๋Œ€ ์œ„์น˜ +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +// FlowLayout ์‚ฌ์šฉ ์‹œ position ๊ด€๋ จ ์Šคํƒ€์ผ ์ œ๊ฑฐ +const baseStyle = isFlowMode ? { + // position, left, top ์—†์Œ + minWidth: size.width, + height: size.height, +} : { + left: `${adjustedPositionX}px`, + top: `${position.y}px`, + position: "absolute", +}; +``` + +--- + +## 5. ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +### 5.1 ์‹œ๋‚˜๋ฆฌ์˜ค 1: ํ™”๋ฉด 68 (๋ฒ„ํŠผ 4๊ฐœ + ํ…Œ์ด๋ธ”) + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1920px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ flex-wrap, justify-end โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์˜ค๋ฅธ์ชฝ ์ •๋ ฌ, ํ…Œ์ด๋ธ” ์ „์ฒด ๋„ˆ๋น„ +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1280px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ flex-wrap, justify-end โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ํฌ๊ธฐ ์œ ์ง€, ํ…Œ์ด๋ธ” ๋„ˆ๋น„ ์กฐ์ • +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (768px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] โ”‚ +โ”‚ [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ โ† ์ž๋™ ์ค„๋ฐ”๊ฟˆ! +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (w-full) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์ค„๋ฐ”๊ฟˆ, ํ…Œ์ด๋ธ” ๋„ˆ๋น„ ์กฐ์ • +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (375px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] โ”‚ +โ”‚ [์ €์žฅ] โ”‚ +โ”‚ [์ˆ˜์ •] โ”‚ +โ”‚ [์‚ญ์ œ] โ”‚ โ† ์„ธ๋กœ ๋ฐฐ์น˜ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” โ”‚ โ”‚ (๊ฐ€๋กœ ์Šคํฌ๋กค) +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: ๋ฒ„ํŠผ ์„ธ๋กœ ๋ฐฐ์น˜, ํ…Œ์ด๋ธ” ๊ฐ€๋กœ ์Šคํฌ๋กค +``` + +### 5.2 ์‹œ๋‚˜๋ฆฌ์˜ค 2: ํ™”๋ฉด 119 (2์—ด ํผ) + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (1920px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์žฅ์น˜ ์ฝ”๋“œ ] [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] โ”‚ +โ”‚ grid-cols-2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ œ์กฐ์‚ฌ ] โ”‚ +โ”‚ col-span-2 (์ „์ฒด ๋„ˆ๋น„) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ’ˆ๋ฒˆ ] [๋ชจ๋ธ๋ช…โ–ผ ] โ”‚ +โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: 2์—ด ๊ทธ๋ฆฌ๋“œ +``` + +**๋ Œ๋”๋ง ๊ฒฐ๊ณผ (768px)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์žฅ์น˜ ์ฝ”๋“œ ] โ”‚ +โ”‚ [์‹œ๋ฆฌ์–ผ๋„˜๋ฒ„ ] โ”‚ โ† 1์—ด๋กœ ๋ณ€๊ฒฝ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ œ์กฐ์‚ฌ ] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [ํ’ˆ๋ฒˆ ] โ”‚ +โ”‚ [๋ชจ๋ธ๋ช…โ–ผ ] โ”‚ +โ”‚ ... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ: 1์—ด ๊ทธ๋ฆฌ๋“œ +``` + +### 5.3 ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋ถ„ํ•  ํŒจ๋„ + +**ํ˜„์žฌ SplitPanelLayout ๋™์ž‘**: +``` +์ขŒ์ธก 60% | ์šฐ์ธก 40% โ† ์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ +``` + +**๋ณ€๊ฒฝ ํ›„ (768px ์ดํ•˜)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ขŒ์ธก 100% โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์šฐ์ธก 100% โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ† ์„ธ๋กœ ๋ฐฐ์น˜๋กœ ์ „ํ™˜ +``` + +**๊ตฌํ˜„**: +```tsx +// SplitPanelLayoutComponent.tsx +const isMobile = useMediaQuery("(max-width: 768px)"); + +return ( +
+
+ {/* ์ขŒ์ธก ํŒจ๋„ */} +
+
+ {/* ์šฐ์ธก ํŒจ๋„ */} +
+
+); +``` + +--- + +## 6. ์—ฃ์ง€ ์ผ€์ด์Šค ๊ฒ€์ฆ + +### 6.1 ๊ฒน์น˜๋Š” ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ ๋ฐ์ดํ„ฐ (ํ™”๋ฉด 74)**: +```json +{ "id": "comp_2606", "position": { "x": 161, "y": 400 } }, // ๋ถ„ํ•  ํŒจ๋„ +{ "id": "comp_fkk75q08", "position": { "x": 161, "y": 400 } } // ๋ผ๋””์˜ค ๋ฒ„ํŠผ +``` + +**๋ฌธ์ œ**: ๊ฐ™์€ ์œ„์น˜์— ๋‘ ์ปดํฌ๋„ŒํŠธ โ†’ z-index๋กœ ๊ฒน์ณ์„œ ํ‘œ์‹œ + +**ํ•ด๊ฒฐ**: +- z-index๊ฐ€ ๋†’์€ ์ปดํฌ๋„ŒํŠธ ์šฐ์„  +- ๋˜๋Š” parent-child ๊ด€๊ณ„๋ฉด ์ค‘์ฒฉ ์ฒ˜๋ฆฌ + +```typescript +function resolveOverlaps(row: Row): Row { + // z-index๋กœ ์ •๋ ฌํ•˜์—ฌ ๋†’์€ ๊ฒƒ๋งŒ ํ‘œ์‹œ + // ๋˜๋Š” parentId ํ™•์ธํ•˜์—ฌ ์ค‘์ฒฉ ์ฒ˜๋ฆฌ +} +``` + +### 6.2 ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ ๋ฐ์ดํ„ฐ (ํ™”๋ฉด 4103)**: +```json +{ + "id": "section-customer-info", + "conditionalConfig": { + "field": "input_method", + "value": "customer_first", + "action": "show" + } +} +``` + +**๋™์ž‘**: ์กฐ๊ฑด์— ๋”ฐ๋ผ show/hide +**Flow ๋ ˆ์ด์•„์›ƒ์—์„œ**: ์ˆจ๊ฒจ์ง€๋ฉด ๊ณต๊ฐ„๋„ ์‚ฌ๋ผ์ง (flex ์ž๋™ ์กฐ์ •) + +โœ… ๋ฌธ์ œ์—†์Œ + +### 6.3 ํ…Œ์ด๋ธ” + ๋ฒ„ํŠผ ์กฐํ•ฉ + +**ํŒจํ„ด**: +``` +[๋ฒ„ํŠผ ๊ทธ๋ฃน] โ† flex-wrap, justify-end +[ํ…Œ์ด๋ธ”] โ† w-full +``` + +**ํ…Œ์ด๋ธ” ๊ฐ€๋กœ ์Šคํฌ๋กค**: +- ํ…Œ์ด๋ธ” ๋‚ด๋ถ€๋Š” ๊ฐ€๋กœ ์Šคํฌ๋กค ์ง€์› +- ์™ธ๋ถ€ ์ปจํ…Œ์ด๋„ˆ๋Š” w-full + +โœ… ๋ฌธ์ œ์—†์Œ + +### 6.4 ์„น์…˜ ์นด๋“œ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ + +**ํ˜„์žฌ**: ์„น์…˜ ์นด๋“œ์™€ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณ„๋„๋กœ ์ €์žฅ๋จ + +**๋ณ€ํ™˜ ์‹œ**: +1. ์„น์…˜ ์นด๋“œ์˜ y ๋ฒ”์œ„ ํŒŒ์•… +2. ํ•ด๋‹น y ๋ฒ”์œ„ ๋‚ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์„น์…˜ ์ž์‹์œผ๋กœ ๊ทธ๋ฃนํ™” +3. ์„น์…˜ ๋‚ด๋ถ€์—์„œ ๋‹ค์‹œ Row ๊ทธ๋ฃนํ™” + +```typescript +function groupWithinSection( + section: Component, + allComponents: Component[] +): Component[] { + const sectionTop = section.position.y; + const sectionBottom = section.position.y + section.size.height; + + return allComponents.filter(comp => { + return comp.id !== section.id && + comp.position.y >= sectionTop && + comp.position.y < sectionBottom; + }); +} +``` + +--- + +## 7. ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ + +### 7.1 ๊ธฐ์กด ๊ธฐ๋Šฅ ํ˜ธํ™˜ + +| ๊ธฐ๋Šฅ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์„ค๋ช… | +|------|----------|------| +| ๋””์ž์ธ ๋ชจ๋“œ | โš ๏ธ ์ˆ˜์ • ํ•„์š” | ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋กœ์ง ์ˆ˜์ • | +| ๋ฏธ๋ฆฌ๋ณด๊ธฐ | โœ… ํ˜ธํ™˜ | Flow ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ๋ Œ๋”๋ง | +| ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ | โœ… ํ˜ธํ™˜ | flex๋กœ ์ž๋™ ์กฐ์ • | +| ๋ถ„ํ•  ํŒจ๋„ | โš ๏ธ ์ˆ˜์ • ํ•„์š” | ๋ฐ˜์‘ํ˜• ์ „ํ™˜ ๋กœ์ง ์ถ”๊ฐ€ | +| ํ…Œ์ด๋ธ” | โœ… ํ˜ธํ™˜ | w-full ์ ์šฉ | +| ๋ชจ๋‹ฌ | โœ… ํ˜ธํ™˜ | ๋ชจ๋‹ฌ ๋‚ด๋ถ€๋„ Flow ์ ์šฉ | + +### 7.2 ๋””์ž์ธ ๋ชจ๋“œ ์ˆ˜์ • + +**ํ˜„์žฌ**: ๋“œ๋ž˜๊ทธํ•˜๋ฉด x, y ํ”ฝ์…€ ์ €์žฅ +**๋ณ€๊ฒฝ ํ›„**: ๋“œ๋ž˜๊ทธํ•˜๋ฉด x, y ํ”ฝ์…€ ์ €์žฅ (๋™์ผ) โ†’ ๋ Œ๋”๋ง ์‹œ ๋ณ€ํ™˜ + +``` +์ €์žฅ: ํ”ฝ์…€ ์ขŒํ‘œ (๊ธฐ์กด ์œ ์ง€) +๋ Œ๋”๋ง: Flow ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€ํ™˜ +``` + +**์žฅ์ **: DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถˆํ•„์š” + +--- + +## 8. ๊ตฌํ˜„ ๊ณ„ํš + +### Phase 1: ํ•ต์‹ฌ ๋ณ€ํ™˜ ๋กœ์ง (1์ผ) + +1. `groupByRows()` ํ•จ์ˆ˜ ๊ตฌํ˜„ +2. `determineJustify()` ํ•จ์ˆ˜ ๊ตฌํ˜„ +3. `FlowLayout` ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ + +### Phase 2: ๋ Œ๋”๋ง ์ ์šฉ (1์ผ) + +1. `DynamicComponentRenderer`์— Flow ๋ชจ๋“œ ์ถ”๊ฐ€ +2. `RealtimePreviewDynamic` ์ˆ˜์ • +3. ๊ธฐ์กด absolute ์Šคํƒ€์ผ ์กฐ๊ฑด๋ถ€ ์ ์šฉ + +### Phase 3: ํŠน์ˆ˜ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ (1์ผ) + +1. ์„น์…˜ ์นด๋“œ ๋‚ด๋ถ€ ๊ทธ๋ฃนํ™” +2. ๊ฒน์น˜๋Š” ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ +3. ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ „ํ™˜ + +### Phase 4: ํ…Œ์ŠคํŠธ (1์ผ) + +1. ํ™”๋ฉด 68 (๋ฒ„ํŠผ + ํ…Œ์ด๋ธ”) ํ…Œ์ŠคํŠธ +2. ํ™”๋ฉด 119 (2์—ด ํผ) ํ…Œ์ŠคํŠธ +3. ํ™”๋ฉด 4103 (๋ณต์žกํ•œ ํผ) ํ…Œ์ŠคํŠธ +4. PC 1920px โ†’ 1280px ํ…Œ์ŠคํŠธ +5. ํƒœ๋ธ”๋ฆฟ 768px ํ…Œ์ŠคํŠธ +6. ๋ชจ๋ฐ”์ผ 375px ํ…Œ์ŠคํŠธ + +--- + +## 9. ์˜ˆ์ƒ ์ด์Šˆ + +### 9.1 ๋””์ž์ด๋„ˆ ์˜๋„ ์†์‹ค + +**๋ฌธ์ œ**: ๋””์ž์ด๋„ˆ๊ฐ€ ์˜๋„์ ์œผ๋กœ ๋ฐฐ์น˜ํ•œ ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Œ + +**ํ•ด๊ฒฐ**: +- ๊ธฐ๋ณธ Flow ๋ ˆ์ด์•„์›ƒ ์ ์šฉ +- ํ•„์š”์‹œ `flexOrder` ์†์„ฑ์œผ๋กœ ์ˆœ์„œ ์กฐ์ • ๊ฐ€๋Šฅ +- ๋˜๋Š” `fixedPosition: true` ์˜ต์…˜์œผ๋กœ ์ ˆ๋Œ€ ์ขŒํ‘œ ์œ ์ง€ + +### 9.2 ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ + +**๋ฌธ์ œ**: ์ผ๋ถ€ ํ™”๋ฉด์€ ์ž์œ  ๋ฐฐ์น˜๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ + +**ํ•ด๊ฒฐ**: +- ํ™”๋ฉด๋ณ„ `layoutMode` ์„ค์ • + - `"flow"`: Flow ๊ธฐ๋ฐ˜ (๊ธฐ๋ณธ๊ฐ’) + - `"absolute"`: ๊ธฐ์กด ์ ˆ๋Œ€ ์ขŒํ‘œ + +### 9.3 ์„ฑ๋Šฅ + +**๋ฌธ์ œ**: ๋งค ๋ Œ๋”๋ง๋งˆ๋‹ค Row ๊ทธ๋ฃนํ™” ๊ณ„์‚ฐ + +**ํ•ด๊ฒฐ**: +- `useMemo`๋กœ ์บ์‹ฑ +- ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์žฌ๊ณ„์‚ฐ + +--- + +## 10. ์ตœ์ข… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ตฌํ˜„ ์ „ + +- [ ] ํ˜„์žฌ ๋™์ž‘ํ•˜๋Š” ํ™”๋ฉด ์Šคํฌ๋ฆฐ์ƒท (๋น„๊ต์šฉ) +- [ ] ํ…Œ์ŠคํŠธ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ • (68, 119, 4103) + +### ๊ตฌํ˜„ ์ค‘ + +- [ ] `groupByRows()` ๊ตฌํ˜„ +- [ ] `determineJustify()` ๊ตฌํ˜„ +- [ ] `FlowLayout` ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ +- [ ] `DynamicComponentRenderer` ์ˆ˜์ • +- [ ] `RealtimePreviewDynamic` ์ˆ˜์ • + +### ํ…Œ์ŠคํŠธ + +- [ ] 1920px ํ…Œ์ŠคํŠธ +- [ ] 1280px ํ…Œ์ŠคํŠธ +- [ ] 768px ํ…Œ์ŠคํŠธ +- [ ] 375px ํ…Œ์ŠคํŠธ +- [ ] ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ +- [ ] ๋ถ„ํ•  ํŒจ๋„ ํ…Œ์ŠคํŠธ +- [ ] ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ…Œ์ŠคํŠธ + +--- + +## 11. ๊ฒฐ๋ก  + +### 11.1 ๊ตฌํ˜„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + +**โœ… ๊ฐ€๋Šฅ** + +- ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์œ ์ง€ (DB ๋ณ€๊ฒฝ ์—†์Œ) +- ๋ Œ๋”๋ง ๋ ˆ๋ฒจ์—์„œ๋งŒ ๋ณ€ํ™˜ +- ๋ชจ๋“  ํ™”๋ฉด ํŒจํ„ด ๋ถ„์„ ์™„๋ฃŒ +- ์—ฃ์ง€ ์ผ€์ด์Šค ํ•ด๊ฒฐ์ฑ… ํ™•๋ณด + +### 11.2 ํ•ต์‹ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ + +``` +Before: position: absolute + left/top ํ”ฝ์…€ +After: Flexbox + flex-wrap + justify-* +``` + +### 11.3 ์˜ˆ์ƒ ํšจ๊ณผ + +| ํ™”๋ฉด ํฌ๊ธฐ | Before | After | +|-----------|--------|-------| +| 1920px | ์ •์ƒ | ์ •์ƒ | +| 1280px | ๋ฒ„ํŠผ ์ž˜๋ฆผ | **์ž๋™ ์กฐ์ •** | +| 768px | ๋ ˆ์ด์•„์›ƒ ๊นจ์ง | **์ž๋™ ์žฌ๋ฐฐ์น˜** | +| 375px | ์‚ฌ์šฉ ๋ถˆ๊ฐ€ | **์ž๋™ ์„ธ๋กœ ๋ฐฐ์น˜** | diff --git a/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md b/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..3d6ec12d --- /dev/null +++ b/docs/DDD1542/PC_RESPONSIVE_IMPLEMENTATION_PLAN.md @@ -0,0 +1,688 @@ +# PC ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ ๊ณ„ํš์„œ + +> ์ž‘์„ฑ์ผ: 2026-01-30 +> ๋ชฉํ‘œ: PC ํ™˜๊ฒฝ (1280px ~ 1920px)์—์„œ ์™„๋ฒฝํ•œ ๋ฐ˜์‘ํ˜• ๊ตฌํ˜„ + +--- + +## 1. ๋ชฉํ‘œ ์ •์˜ + +### 1.1 ๋ฒ”์œ„ + +| ํ™˜๊ฒฝ | ํ™”๋ฉด ํฌ๊ธฐ | ์šฐ์„ ์ˆœ์œ„ | +|------|-----------|----------| +| **PC (๋Œ€ํ˜• ๋ชจ๋‹ˆํ„ฐ)** | 1920px | ๊ธฐ์ค€ | +| **PC (๋…ธํŠธ๋ถ)** | 1280px ~ 1440px | **1์ˆœ์œ„** | +| ํƒœ๋ธ”๋ฆฟ | 768px ~ 1024px | 2์ˆœ์œ„ (์ถ”ํ›„) | +| ๋ชจ๋ฐ”์ผ | < 768px | 3์ˆœ์œ„ (์ถ”ํ›„) | + +### 1.2 ๋ชฉํ‘œ ๋™์ž‘ + +``` +1920px ํ™”๋ฉด์—์„œ ๋””์ž์ธ + โ†“ +1280px ํ™”๋ฉด์œผ๋กœ ์ถ•์†Œ + โ†“ +์ปดํฌ๋„ŒํŠธ๋“ค์ด ๋น„์œจ์— ๋งž๊ฒŒ ์žฌ๋ฐฐ์น˜ (์œ„์น˜, ํฌ๊ธฐ ๋ชจ๋‘) + โ†“ +๋ ˆ์ด์•„์›ƒ ๊นจ์ง€์ง€ ์•Š์Œ +``` + +### 1.3 ์„ฑ๊ณต ๊ธฐ์ค€ + +- [ ] 1920px์—์„œ ๋””์ž์ธํ•œ ํ™”๋ฉด์ด 1280px์—์„œ ์ •์ƒ ํ‘œ์‹œ +- [ ] ๋ฒ„ํŠผ์ด ํ™”๋ฉด ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€์ง€ ์•Š์Œ +- [ ] ํ…Œ์ด๋ธ”์ด ํ™”๋ฉด ๋„ˆ๋น„์— ๋งž๊ฒŒ ์กฐ์ •๋จ +- [ ] ๋ถ„ํ•  ํŒจ๋„์ด ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ถ•์†Œ๋จ + +--- + +## 2. ํ˜„์žฌ ์‹œ์Šคํ…œ ๋ถ„์„ + +### 2.1 ๋ Œ๋”๋ง ํ๋ฆ„ (ํ˜„์žฌ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. API ํ˜ธ์ถœ โ”‚ +โ”‚ screenApi.getLayoutV2(screenId) โ”‚ +โ”‚ โ†’ screen_layouts_v2.layout_data (JSONB) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ โ”‚ +โ”‚ convertV2ToLegacy(v2Response) โ”‚ +โ”‚ โ†’ components ๋ฐฐ์—ด (position, size ํฌํ•จ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. ์Šค์ผ€์ผ ๊ณ„์‚ฐ (page.tsx ๋ผ์ธ 395-460) โ”‚ +โ”‚ const designWidth = layout.screenResolution.width || 1200โ”‚ +โ”‚ const newScale = containerWidth / designWidth โ”‚ +โ”‚ โ†’ ์ „์ฒด ํ™”๋ฉด์„ scale()๋กœ ์ถ•์†Œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 4. ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง (RealtimePreviewDynamic.tsx ๋ผ์ธ 524-536) โ”‚ +โ”‚ left: `${position.x}px` โ† ํ”ฝ์…€ ๊ณ ์ • โ”‚ +โ”‚ top: `${position.y}px` โ† ํ”ฝ์…€ ๊ณ ์ • โ”‚ +โ”‚ position: absolute โ† ์ ˆ๋Œ€ ์œ„์น˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.2 ํ˜„์žฌ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์  + +**ํ˜„์žฌ**: `transform: scale()` ๋ฐฉ์‹ +```tsx +// page.tsx ๋ผ์ธ 515-520 +
+``` + +| ๋ฌธ์ œ | ์„ค๋ช… | +|------|------| +| **์ถ•์†Œ๋งŒ ๋จ** | ๋ ˆ์ด์•„์›ƒ ์žฌ๋ฐฐ์น˜ ์—†์Œ | +| **ํฐํŠธ ์ž‘์•„์ง** | ์ „์ฒด scale๋กœ ํฐํŠธ๋„ ์ถ•์†Œ | +| **ํด๋ฆญ ์˜์—ญ ์˜ค์ฐจ** | scale ์ ์šฉ ์‹œ ํด๋ฆญ ์œ„์น˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ | +| **์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• ์•„๋‹˜** | ๋น„์œจ๋งŒ ์œ ์ง€, ๋ ˆ์ด์•„์›ƒ ์ตœ์ ํ™” ์—†์Œ | + +### 2.3 position.x, position.y ์‚ฌ์šฉ ์œ„์น˜ + +| ํŒŒ์ผ | ๋ผ์ธ | ์šฉ๋„ | +|------|------|------| +| `RealtimePreviewDynamic.tsx` | 524-526 | ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ์Šคํƒ€์ผ | +| `AutoRegisteringComponentRenderer.ts` | 42-43 | ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ | +| `page.tsx` | 744-745 | ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ | +| `ScreenDesigner.tsx` | 2890-2894 | ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์œ„์น˜ | +| `ScreenModal.tsx` | 620-621 | ๋ชจ๋‹ฌ ๋‚ด ์˜คํ”„์…‹ ์กฐ์ • | + +--- + +## 3. ๊ตฌํ˜„ ๋ฐฉ์‹: ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜ + +### 3.1 ํ•ต์‹ฌ ์•„์ด๋””์–ด + +``` +ํ”ฝ์…€ ์ขŒํ‘œ (1920px ๊ธฐ์ค€) + โ†“ +ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ + โ†“ +ํ™”๋ฉด ํฌ๊ธฐ์— ๊ด€๊ณ„์—†์ด ๋น„์œจ ์œ ์ง€ +``` + +**์˜ˆ์‹œ**: +``` +๋ฒ„ํŠผ ์œ„์น˜: x=1753px (1920px ๊ธฐ์ค€) + โ†“ +ํผ์„ผํŠธ: 1753 / 1920 = 91.3% + โ†“ +1280px ํ™”๋ฉด: 1280 * 0.913 = 1168px + โ†“ +๋ฒ„ํŠผ์ด ํ™”๋ฉด ์•ˆ์— ์ •์ƒ ํ‘œ์‹œ +``` + +### 3.2 ๋ณ€ํ™˜ ๊ณต์‹ + +```typescript +// ํ”ฝ์…€ โ†’ ํผ์„ผํŠธ ๋ณ€ํ™˜ +const DESIGN_WIDTH = 1920; + +function toPercent(pixelX: number): string { + return `${(pixelX / DESIGN_WIDTH) * 100}%`; +} + +// ์‚ฌ์šฉ +left: toPercent(position.x) // "91.3%" +width: toPercent(size.width) // "8.2%" +``` + +### 3.3 Y์ถ• ์ฒ˜๋ฆฌ + +Y์ถ•์€ ๋‘ ๊ฐ€์ง€ ์˜ต์…˜: + +**์˜ต์…˜ A: Y์ถ•๋„ ํผ์„ผํŠธ (๊ถŒ์žฅ)** +```typescript +const DESIGN_HEIGHT = 1080; +top: `${(position.y / DESIGN_HEIGHT) * 100}%` +``` + +**์˜ต์…˜ B: Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€** +```typescript +top: `${position.y}px` // ์„ธ๋กœ๋Š” ์Šคํฌ๋กค๋กœ ํ•ด๊ฒฐ +``` + +**๊ฒฐ์ •: ์˜ต์…˜ B (Y์ถ• ํ”ฝ์…€ ์œ ์ง€)** +- ์ด์œ : ์„ธ๋กœ ์Šคํฌ๋กค์€ ์ž์—ฐ์Šค๋Ÿฌ์›€ +- ๊ฐ€๋กœ๋งŒ ๋ฐ˜์‘ํ˜•์ด๋ฉด PC ํ™˜๊ฒฝ์—์„œ ์ถฉ๋ถ„ + +--- + +## 4. ๊ตฌํ˜„ ์ƒ์„ธ + +### 4.1 ์ˆ˜์ • ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `RealtimePreviewDynamic.tsx` | left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ | +| `AutoRegisteringComponentRenderer.ts` | left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ | +| `page.tsx` | scale ์ œ๊ฑฐ, ์ปจํ…Œ์ด๋„ˆ width: 100% | + +### 4.2 RealtimePreviewDynamic.tsx ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 524-530)**: +```tsx +const baseStyle = { + left: `${adjustedPositionX}px`, + top: `${position.y}px`, + width: displayWidth, + height: displayHeight, + zIndex: component.type === "layout" ? 1 : position.z || 2, +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +const DESIGN_WIDTH = 1920; + +const baseStyle = { + left: `${(adjustedPositionX / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + top: `${position.y}px`, // Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€ + width: `${(parseFloat(displayWidth) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + height: displayHeight, // ๋†’์ด๋Š” ํ”ฝ์…€ ์œ ์ง€ + zIndex: component.type === "layout" ? 1 : position.z || 2, +}; +``` + +### 4.3 AutoRegisteringComponentRenderer.ts ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 40-48)**: +```tsx +const baseStyle: React.CSSProperties = { + position: "absolute", + left: `${component.position?.x || 0}px`, + top: `${component.position?.y || 0}px`, + width: `${component.size?.width || 200}px`, + height: `${component.size?.height || 36}px`, + zIndex: component.position?.z || 1, +}; +``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +const DESIGN_WIDTH = 1920; + +const baseStyle: React.CSSProperties = { + position: "absolute", + left: `${((component.position?.x || 0) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + top: `${component.position?.y || 0}px`, // Y์ถ•์€ ํ”ฝ์…€ ์œ ์ง€ + width: `${((component.size?.width || 200) / DESIGN_WIDTH) * 100}%`, // ํผ์„ผํŠธ + height: `${component.size?.height || 36}px`, // ๋†’์ด๋Š” ํ”ฝ์…€ ์œ ์ง€ + zIndex: component.position?.z || 1, +}; +``` + +### 4.4 page.tsx ์ˆ˜์ • + +**ํ˜„์žฌ (๋ผ์ธ 515-528)**: +```tsx +
+``` + +**๋ณ€๊ฒฝ ํ›„**: +```tsx +
+``` + +### 4.5 ๊ณตํ†ต ์ƒ์ˆ˜ ํŒŒ์ผ ์ƒ์„ฑ + +```typescript +// frontend/lib/constants/responsive.ts + +export const RESPONSIVE_CONFIG = { + DESIGN_WIDTH: 1920, + DESIGN_HEIGHT: 1080, + MIN_WIDTH: 1280, + MAX_WIDTH: 1920, +} as const; + +export function toPercentX(pixelX: number): string { + return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} + +export function toPercentWidth(pixelWidth: number): string { + return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} +``` + +--- + +## 5. ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +### 5.1 ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ๋‚˜๋ฆฌ์˜ค + +**ํ…Œ์ŠคํŠธ ํ™”๋ฉด**: screen_id = 68 (์ˆ˜์ฃผ ๋ชฉ๋ก) +```json +{ + "components": [ + { + "id": "comp_1895", + "url": "v2-table-list", + "position": { "x": 8, "y": 128 }, + "size": { "width": 1904, "height": 600 } + }, + { + "id": "comp_1896", + "url": "v2-button-primary", + "position": { "x": 1753, "y": 88 }, + "size": { "width": 158, "height": 40 } + }, + { + "id": "comp_1897", + "url": "v2-button-primary", + "position": { "x": 1594, "y": 88 }, + "size": { "width": 158, "height": 40 } + }, + { + "id": "comp_1898", + "url": "v2-button-primary", + "position": { "x": 1436, "y": 88 }, + "size": { "width": 158, "height": 40 } + } + ] +} +``` + +### 5.2 ํ˜„์žฌ ๋ฐฉ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +**1920px ํ™”๋ฉด**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ 1277 1436 1594 1753 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ x=8 x=1904 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 1904px) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ ํ‘œ์‹œ +``` + +**1280px ํ™”๋ฉด (ํ˜„์žฌ scale ๋ฐฉ์‹)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ scale(0.67) ์ ์šฉ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [๋ถ„๋ฆฌ][์ €][์ˆ˜][์‚ญ] โ”‚ โ”‚ โ† ์ „์ฒด ์ถ•์†Œ, ํฐํŠธ ์ž‘์•„์ง +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ํ…Œ์ด๋ธ” (์ถ•์†Œ๋จ) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ (์—ฌ๋ฐฑ ๋ฐœ์ƒ) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โš ๏ธ ์ž‘๋™ํ•˜์ง€๋งŒ ํฐํŠธ/์—ฌ๋ฐฑ ๋ฌธ์ œ +``` + +### 5.3 ํผ์„ผํŠธ ๋ฐฉ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + +**๋ณ€ํ™˜ ๊ณ„์‚ฐ**: +``` +ํ…Œ์ด๋ธ”: + x: 8px โ†’ 8/1920 = 0.42% + width: 1904px โ†’ 1904/1920 = 99.17% + +์‚ญ์ œ ๋ฒ„ํŠผ: + x: 1753px โ†’ 1753/1920 = 91.30% + width: 158px โ†’ 158/1920 = 8.23% + +์ˆ˜์ • ๋ฒ„ํŠผ: + x: 1594px โ†’ 1594/1920 = 83.02% + width: 158px โ†’ 158/1920 = 8.23% + +์ €์žฅ ๋ฒ„ํŠผ: + x: 1436px โ†’ 1436/1920 = 74.79% + width: 158px โ†’ 158/1920 = 8.23% + +๋ถ„๋ฆฌ ๋ฒ„ํŠผ: + x: 1277px โ†’ 1277/1920 = 66.51% + width: 158px โ†’ 158/1920 = 8.23% +``` + +**1920px ํ™”๋ฉด**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ] [์ €์žฅ] [์ˆ˜์ •] [์‚ญ์ œ] โ”‚ +โ”‚ 66.5% 74.8% 83.0% 91.3% โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 0.42% 99.6% โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 99.17%) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ์ •์ƒ ํ‘œ์‹œ (1920px์™€ ๋™์ผ) +``` + +**1280px ํ™”๋ฉด (ํผ์„ผํŠธ ๋ฐฉ์‹)**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋ถ„๋ฆฌ][์ €์žฅ][์ˆ˜์ •][์‚ญ์ œ] โ”‚ +โ”‚ 66.5% 74.8% 83.0% 91.3% โ”‚ +โ”‚ = 851 957 1063 1169 โ”‚ โ† ํ™”๋ฉด ์•ˆ์— ํ‘œ์‹œ! +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 0.42% 99.6% โ”‚ +โ”‚ = 5px = 1275 โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ…Œ์ด๋ธ” (width: 99.17%) โ”‚ โ”‚ โ† ํ™”๋ฉด ๋„ˆ๋น„์— ๋งž๊ฒŒ ์กฐ์ • +โ”‚ โ”‚ = 1280 * 0.9917 = 1269px โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โœ… ๋น„์œจ ์œ ์ง€, ํ™”๋ฉด ์•ˆ์— ํ‘œ์‹œ, ํฐํŠธ ํฌ๊ธฐ ์œ ์ง€ +``` + +### 5.4 ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ๊ฒ€์ฆ + +**1920px**: +``` +๋ถ„๋ฆฌ: 1277px, ๋„ˆ๋น„ 158px โ†’ ๋: 1435px +์ €์žฅ: 1436px (๊ฐ„๊ฒฉ: 1px) +์ˆ˜์ •: 1594px (๊ฐ„๊ฒฉ: 1px) +์‚ญ์ œ: 1753px (๊ฐ„๊ฒฉ: 1px) +``` + +**1280px (ํผ์„ผํŠธ ๋ณ€ํ™˜ ํ›„)**: +``` +๋ถ„๋ฆฌ: 1280 * 0.665 = 851px, ๋„ˆ๋น„ 1280 * 0.082 = 105px โ†’ ๋: 956px +์ €์žฅ: 1280 * 0.748 = 957px (๊ฐ„๊ฒฉ: 1px) โœ… +์ˆ˜์ •: 1280 * 0.830 = 1063px (๊ฐ„๊ฒฉ: 1px) โœ… +์‚ญ์ œ: 1280 * 0.913 = 1169px (๊ฐ„๊ฒฉ: 1px) โœ… +``` + +**๊ฒฐ๋ก **: ๋ฒ„ํŠผ ๊ฐ„๊ฒฉ ๋น„์œจ๋„ ์œ ์ง€๋จ + +--- + +## 6. ์—ฃ์ง€ ์ผ€์ด์Šค ๊ฒ€์ฆ + +### 6.1 ๋ถ„ํ•  ํŒจ๋„ (SplitPanelLayout) + +**ํ˜„์žฌ ๋™์ž‘**: +- ์ขŒ์ธก ํŒจ๋„: 60% ๋„ˆ๋น„ +- ์šฐ์ธก ํŒจ๋„: 40% ๋„ˆ๋น„ +- **์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜!** + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ์ขŒ์ธก 1152px, ์šฐ์ธก 768px +1280px: ์ขŒ์ธก 768px, ์šฐ์ธก 512px +โœ… ์ž๋™์œผ๋กœ ๋น„์œจ ์œ ์ง€๋จ +``` + +**๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ**: +- ๋ฌธ์ œ: ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ”ฝ์…€ ๊ณ ์ •์ด๋ฉด ๊นจ์ง +- ํ•ด๊ฒฐ: ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€๋„ ํผ์„ผํŠธ ์ ์šฉ ํ•„์š” + +### 6.2 ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ (TableList) + +**ํ˜„์žฌ**: +- ํ…Œ์ด๋ธ” ์ž์ฒด๋Š” ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„ 100% ์‚ฌ์šฉ +- ์ปฌ๋Ÿผ ๋„ˆ๋น„๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์กฐ์ • + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ width: 99.17% = 1904px +1280px: ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ width: 99.17% = 1269px +โœ… ํ…Œ์ด๋ธ”์ด ์ž๋™์œผ๋กœ ์กฐ์ •๋จ +``` + +### 6.3 ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ + +**ํ˜„์žฌ ์ฝ”๋“œ (page.tsx ๋ผ์ธ 744-745)**: +```typescript +const relativeChildComponent = { + position: { + x: child.position.x - component.position.x, + y: child.position.y - component.position.y, + }, +}; +``` + +**๋ฌธ์ œ**: ์ƒ๋Œ€ ์ขŒํ‘œ๋„ ํ”ฝ์…€ ๊ธฐ๋ฐ˜ + +**ํ•ด๊ฒฐ**: ๋ถ€๋ชจ ๊ธฐ์ค€ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ +```typescript +const relativeChildComponent = { + position: { + // ๋ถ€๋ชจ ๋„ˆ๋น„ ๊ธฐ์ค€ ํผ์„ผํŠธ + xPercent: ((child.position.x - component.position.x) / component.size.width) * 100, + y: child.position.y - component.position.y, + }, +}; +``` + +### 6.4 ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ (๋””์ž์ธ ๋ชจ๋“œ) + +**ScreenDesigner.tsx**: +- ๋“œ๋กญ ์œ„์น˜๋Š” ์—ฌ์ „ํžˆ ํ”ฝ์…€๋กœ ์ €์žฅ +- ๋ Œ๋”๋ง ์‹œ์—๋งŒ ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜ +- **์ €์žฅ ๋ฐฉ์‹ ๋ณ€๊ฒฝ ์—†์Œ!** + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1. ๋””์ž์ด๋„ˆ๊ฐ€ 1920px ํ™”๋ฉด์—์„œ ๋ฒ„ํŠผ ๋“œ๋กญ +2. position: { x: 1753, y: 88 } ์ €์žฅ (ํ”ฝ์…€) +3. ๋ Œ๋”๋ง ์‹œ 91.3%๋กœ ๋ณ€ํ™˜ +4. 1280px ํ™”๋ฉด์—์„œ๋„ ์ •์ƒ ํ‘œ์‹œ +โœ… ๋””์ž์ธ ๋ชจ๋“œ ํ˜ธํ™˜ +``` + +### 6.5 ๋ชจ๋‹ฌ ๋‚ด ํ™”๋ฉด + +**ScreenModal.tsx (๋ผ์ธ 620-621)**: +```typescript +x: parseFloat(component.position?.x?.toString() || "0") - offsetX, +y: parseFloat(component.position?.y?.toString() || "0") - offsetY, +``` + +**๋ฌธ์ œ**: ์˜คํ”„์…‹ ๊ณ„์‚ฐ์ด ํ”ฝ์…€ ๊ธฐ๋ฐ˜ + +**ํ•ด๊ฒฐ**: ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ๋„ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ +```typescript +// ๋ชจ๋‹ฌ ์ปจํ…Œ์ด๋„ˆ ๋„ˆ๋น„ ๊ธฐ์ค€์œผ๋กœ ํผ์„ผํŠธ ๊ณ„์‚ฐ +const modalWidth = containerRef.current?.clientWidth || DESIGN_WIDTH; +const xPercent = ((position.x - offsetX) / DESIGN_WIDTH) * 100; +``` + +--- + +## 7. ์ž ์žฌ์  ๋ฌธ์ œ ๋ฐ ํ•ด๊ฒฐ์ฑ… + +### 7.1 ์ตœ์†Œ ๋„ˆ๋น„ ๋ฌธ์ œ + +**๋ฌธ์ œ**: ๋ฒ„ํŠผ์ด ๋„ˆ๋ฌด ์ž‘์•„์งˆ ์ˆ˜ ์žˆ์Œ +``` +158px ๋ฒ„ํŠผ โ†’ 1280px ํ™”๋ฉด์—์„œ 105px +โ†’ ํ…์ŠคํŠธ๊ฐ€ ์ž˜๋ฆด ์ˆ˜ ์žˆ์Œ +``` + +**ํ•ด๊ฒฐ**: min-width ์„ค์ • +```css +min-width: 80px; +``` + +### 7.2 ๊ฒน์นจ ๋ฌธ์ œ + +**๋ฌธ์ œ**: ํ™”๋ฉด์ด ์ž‘์•„์ง€๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฒน์น  ์ˆ˜ ์žˆ์Œ + +**์‹œ๋ฎฌ๋ ˆ์ด์…˜**: +``` +1920px: ๋ฒ„ํŠผ 4๊ฐœ๊ฐ€ ๊ฐ„๊ฒฉ 1px๋กœ ๋ฐฐ์น˜ +1280px: ๋ฒ„ํŠผ 4๊ฐœ๊ฐ€ ๊ฐ„๊ฒฉ 1px๋กœ ๋ฐฐ์น˜ (๋น„์œจ ์œ ์ง€) +โœ… ๊ฒน์น˜์ง€ ์•Š์Œ (๊ฐ„๊ฒฉ๋„ ๋น„์œจ๋กœ ์ถ•์†Œ) +``` + +### 7.3 ํฐํŠธ ํฌ๊ธฐ + +**ํ˜„์žฌ**: ํฐํŠธ๋Š” px ๊ณ ์ • +**๋ณ€๊ฒฝ ํ›„**: ํฐํŠธ ํฌ๊ธฐ ์œ ์ง€ (scale์ด ์•„๋‹ˆ๋ฏ€๋กœ) + +**๊ฒฐ๊ณผ**: ํฐํŠธ ํฌ๊ธฐ๋Š” ๊ทธ๋Œ€๋กœ, ๋ ˆ์ด์•„์›ƒ๋งŒ ๋น„์œจ ์กฐ์ • +โœ… ๊ฐ€๋…์„ฑ ์œ ์ง€ + +### 7.4 height ์ฒ˜๋ฆฌ + +**๊ฒฐ์ •**: height๋Š” ํ”ฝ์…€ ์œ ์ง€ +- ์ด์œ : ์„ธ๋กœ ์Šคํฌ๋กค์€ ์ž์—ฐ์Šค๋Ÿฌ์›€ +- ์„ธ๋กœ ๋ฐ˜์‘ํ˜•์€ ๋ถˆํ•„์š” (PC ํ™˜๊ฒฝ) + +--- + +## 8. ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ + +### 8.1 ๊ธฐ์กด ํ™”๋ฉด ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| ์ผ๋ฐ˜ ๋ฒ„ํŠผ | โœ… | ํผ์„ผํŠธ๋กœ ๋ณ€ํ™˜, ์œ„์น˜ ์œ ์ง€ | +| ํ…Œ์ด๋ธ” | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | +| ๋ถ„ํ•  ํŒจ๋„ | โœ… | ์ด๋ฏธ ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ | +| ํƒญ ๋ ˆ์ด์•„์›ƒ | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | +| ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ | โœ… | ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ | +| ์ธํ’‹ ํ•„๋“œ | โœ… | ์ปจํ…Œ์ด๋„ˆ ๋น„์œจ ์œ ์ง€ | + +### 8.2 ๋””์ž์ธ ๋ชจ๋“œ ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ | โœ… | ์ €์žฅ์€ ํ”ฝ์…€, ๋ Œ๋”๋ง๋งŒ ํผ์„ผํŠธ | +| ๋ฆฌ์‚ฌ์ด์ฆˆ | โœ… | ์ €์žฅ์€ ํ”ฝ์…€, ๋ Œ๋”๋ง๋งŒ ํผ์„ผํŠธ | +| ๊ทธ๋ฆฌ๋“œ ์Šค๋ƒ… | โœ… | ์Šค๋ƒ…์€ ํ”ฝ์…€ ๊ธฐ์ค€ ์œ ์ง€ | +| ๋ฏธ๋ฆฌ๋ณด๊ธฐ | โœ… | ๋ Œ๋”๋ง ๋™์ผ ๋ฐฉ์‹ | + +### 8.3 API ํ˜ธํ™˜ + +| ํ•ญ๋ชฉ | ํ˜ธํ™˜ ์—ฌ๋ถ€ | ์ด์œ  | +|------|----------|------| +| DB ์ €์žฅ | โœ… | ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ (ํ”ฝ์…€ ์ €์žฅ) | +| API ์‘๋‹ต | โœ… | ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์—†์Œ | +| V2 ๋ณ€ํ™˜ | โœ… | ๋ณ€ํ™˜ ๋กœ์ง ๋ณ€๊ฒฝ ์—†์Œ | + +--- + +## 9. ๊ตฌํ˜„ ์ˆœ์„œ + +### Phase 1: ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ƒ์„ฑ (30๋ถ„) + +```typescript +// frontend/lib/constants/responsive.ts +export const RESPONSIVE_CONFIG = { + DESIGN_WIDTH: 1920, +} as const; + +export function toPercentX(pixelX: number): string { + return `${(pixelX / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} + +export function toPercentWidth(pixelWidth: number): string { + return `${(pixelWidth / RESPONSIVE_CONFIG.DESIGN_WIDTH) * 100}%`; +} +``` + +### Phase 2: RealtimePreviewDynamic.tsx ์ˆ˜์ • (1์‹œ๊ฐ„) + +1. import ์ถ”๊ฐ€ +2. baseStyle์˜ left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ +3. ๋ถ„ํ•  ํŒจ๋„ ์œ„ ๋ฒ„ํŠผ ์กฐ์ • ๋กœ์ง๋„ ํผ์„ผํŠธ ์ ์šฉ + +### Phase 3: AutoRegisteringComponentRenderer.ts ์ˆ˜์ • (30๋ถ„) + +1. import ์ถ”๊ฐ€ +2. getComponentStyle()์˜ left, width๋ฅผ ํผ์„ผํŠธ๋กœ ๋ณ€๊ฒฝ + +### Phase 4: page.tsx ์ˆ˜์ • (1์‹œ๊ฐ„) + +1. scale ๋กœ์ง ์ œ๊ฑฐ ๋˜๋Š” ์ˆ˜์ • +2. ์ปจํ…Œ์ด๋„ˆ width: 100%๋กœ ๋ณ€๊ฒฝ +3. ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์ƒ๋Œ€ ์œ„์น˜ ๊ณ„์‚ฐ ์ˆ˜์ • + +### Phase 5: ํ…Œ์ŠคํŠธ (1์‹œ๊ฐ„) + +1. 1920px ํ™”๋ฉด์—์„œ ๊ธฐ์กด ํ™”๋ฉด ์ •์ƒ ๋™์ž‘ ํ™•์ธ +2. 1280px ํ™”๋ฉด์œผ๋กœ ์ถ•์†Œ ํ…Œ์ŠคํŠธ +3. ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +4. ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ + +--- + +## 10. ์ตœ์ข… ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### ๊ตฌํ˜„ ์ „ + +- [ ] ํ˜„์žฌ ๋™์ž‘ํ•˜๋Š” ํ™”๋ฉด ์Šคํฌ๋ฆฐ์ƒท ์บก์ฒ˜ (๋น„๊ต์šฉ) +- [ ] ํ…Œ์ŠคํŠธ ํ™”๋ฉด ๋ชฉ๋ก ์„ ์ • + +### ๊ตฌํ˜„ ์ค‘ + +- [ ] responsive.ts ์ƒ์„ฑ +- [ ] RealtimePreviewDynamic.tsx ์ˆ˜์ • +- [ ] AutoRegisteringComponentRenderer.ts ์ˆ˜์ • +- [ ] page.tsx ์ˆ˜์ • + +### ๊ตฌํ˜„ ํ›„ + +- [ ] 1920px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] 1440px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] 1280px ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ๋””์ž์ธ ๋ชจ๋“œ ํ…Œ์ŠคํŠธ +- [ ] ๋ชจ๋‹ฌ ๋‚ด ํ™”๋ฉด ํ…Œ์ŠคํŠธ + +--- + +## 11. ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ + +| ์ž‘์—… | ์‹œ๊ฐ„ | +|------|------| +| ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ƒ์„ฑ | 30๋ถ„ | +| RealtimePreviewDynamic.tsx | 1์‹œ๊ฐ„ | +| AutoRegisteringComponentRenderer.ts | 30๋ถ„ | +| page.tsx | 1์‹œ๊ฐ„ | +| ํ…Œ์ŠคํŠธ | 1์‹œ๊ฐ„ | +| **ํ•ฉ๊ณ„** | **4์‹œ๊ฐ„** | + +--- + +## 12. ๊ฒฐ๋ก  + +**ํผ์„ผํŠธ ๊ธฐ๋ฐ˜ ๋ฐฐ์น˜**๊ฐ€ PC ๋ฐ˜์‘ํ˜•์˜ ๊ฐ€์žฅ ํ™•์‹คํ•œ ํ•ด๊ฒฐ์ฑ…์ž…๋‹ˆ๋‹ค. + +| ํ•ญ๋ชฉ | scale ๋ฐฉ์‹ | ํผ์„ผํŠธ ๋ฐฉ์‹ | +|------|-----------|------------| +| ํฐํŠธ ํฌ๊ธฐ | ์ถ•์†Œ๋จ | **์œ ์ง€** | +| ๋ ˆ์ด์•„์›ƒ ๋น„์œจ | ์œ ์ง€ | **์œ ์ง€** | +| ํด๋ฆญ ์˜์—ญ | ์˜ค์ฐจ ๊ฐ€๋Šฅ | **์ •ํ™•** | +| ๊ตฌํ˜„ ๋ณต์žก๋„ | ๋‚ฎ์Œ | **์ค‘๊ฐ„** | +| ์ง„์ •ํ•œ ๋ฐ˜์‘ํ˜• | โŒ | **โœ…** | + +**DB ๋ณ€๊ฒฝ ์—†์ด, ๋ Œ๋”๋ง ๋กœ์ง๋งŒ ์ˆ˜์ •**ํ•˜์—ฌ ์™„๋ฒฝํ•œ PC ๋ฐ˜์‘ํ˜•์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md index 42cd872b..411fdd1f 100644 --- a/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md +++ b/docs/DDD1542/RESPONSIVE_GRID_SYSTEM_ARCHITECTURE.md @@ -103,6 +103,162 @@ - ๋ถ„ํ•  ํŒจ๋„ ๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ ``` +### 2.5 ๋ ˆ์ด์•„์›ƒ ์‹œ์Šคํ…œ ๊ตฌ์กฐ + +ํ˜„์žฌ ์‹œ์Šคํ…œ์—๋Š” ๋‘ ๊ฐ€์ง€ ๋ ˆ๋ฒจ์˜ ๋ ˆ์ด์•„์›ƒ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค: + +#### 2.5.1 ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ (screen_layouts_v2) + +ํ™”๋ฉด ์ „์ฒด์˜ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + +```json +// DB ๊ตฌ์กฐ +{ + "version": "2.0", + "components": [ + { "id": "comp_1", "position": { "x": 100, "y": 50 }, ... }, + { "id": "comp_2", "position": { "x": 500, "y": 50 }, ... }, + { "id": "GridLayout_1", "position": { "x": 100, "y": 200 }, ... } + ] +} +``` + +**ํ˜„์žฌ**: absolute ํฌ์ง€์…˜์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ โ†’ **๋ฐ˜์‘ํ˜• ๋ถˆ๊ฐ€** + +#### 2.5.2 ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ (GridLayout, FlexboxLayout ๋“ฑ) + +๊ฐœ๋ณ„ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ zone ๋ฐฐ์น˜๋ฅผ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + +| ์ปดํฌ๋„ŒํŠธ | ์œ„์น˜ | ๋‚ด๋ถ€ ๊ตฌ์กฐ | CSS Grid ์‚ฌ์šฉ | +|----------|------|-----------|---------------| +| `GridLayout` | `layouts/grid/` | zones ๋ฐฐ์—ด | โœ… ์ด๋ฏธ ์‚ฌ์šฉ | +| `FlexboxLayout` | `layouts/flexbox/` | zones ๋ฐฐ์—ด | โŒ absolute | +| `SplitLayout` | `layouts/split/` | left/right | โŒ flex | +| `TabsLayout` | `layouts/` | tabs ๋ฐฐ์—ด | โŒ ํƒญ ๊ตฌ์กฐ | +| `CardLayout` | `layouts/card-layout/` | zones ๋ฐฐ์—ด | โŒ flex | +| `AccordionLayout` | `layouts/accordion/` | items ๋ฐฐ์—ด | โŒ ์•„์ฝ”๋””์–ธ | + +#### 2.5.3 ๊ตฌ์กฐ ๋‹ค์ด์–ด๊ทธ๋žจ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ screen_layouts_v2 (ํ™”๋ฉด ์ „์ฒด) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํ˜„์žฌ: absolute ํฌ์ง€์…˜ โ†’ ๋ฐ˜์‘ํ˜• ๋ถˆ๊ฐ€ โ”‚ โ”‚ +โ”‚ โ”‚ ๋ณ€๊ฒฝ: ResponsiveGridLayout (CSS Grid) โ†’ ๋ฐ˜์‘ํ˜• ๊ฐ€๋Šฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ v2-button โ”‚ โ”‚ v2-input โ”‚ โ”‚ GridLayout (์ปดํฌ๋„ŒํŠธ) โ”‚ โ”‚ +โ”‚ โ”‚ (shadcn) โ”‚ โ”‚ (shadcn) โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ zone1 โ”‚ zone2 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (์ด๋ฏธ โ”‚ (์ด๋ฏธ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ CSS Gridโ”‚ CSS Grid) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.6 ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ํ˜ธํ™˜์„ฑ + +#### 2.6.1 GridLayout (๊ธฐ์กด ์ปค์Šคํ…€ ๊ทธ๋ฆฌ๋“œ) + +```tsx +// frontend/lib/registry/layouts/grid/GridLayout.tsx +// ์ด๋ฏธ CSS Grid๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ! + +const gridStyle: React.CSSProperties = { + display: "grid", + gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`, + gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`, + gap: `${gridConfig.gap || 16}px`, +}; +``` + +**ํ˜ธํ™˜์„ฑ**: โœ… **์™„์ „ ํ˜ธํ™˜** +- GridLayout์€ ํ™”๋ฉด ๋‚ด ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ์ทจ๊ธ‰๋จ +- ResponsiveGridLayout์ด GridLayout์˜ **์œ„์น˜๋งŒ** ๊ด€๋ฆฌ +- GridLayout ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ๊ทธ๋Œ€๋กœ ๋™์ž‘ + +#### 2.6.2 FlexboxLayout + +```tsx +// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx +// zone ๋‚ด๋ถ€์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ absolute๋กœ ๋ฐฐ์น˜ + +{zoneChildren.map((child) => ( +
+ {renderer.renderChild(child)} +
+))} +``` + +**ํ˜ธํ™˜์„ฑ**: โœ… **ํ˜ธํ™˜** (๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€) +- FlexboxLayout ์ปดํฌ๋„ŒํŠธ ์ž์ฒด์˜ ์œ„์น˜๋Š” ResponsiveGridLayout์ด ๊ด€๋ฆฌ +- ๋‚ด๋ถ€ zone์˜ ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜๋Š” ๊ธฐ์กด absolute ๋ฐฉ์‹ ์œ ์ง€ + +#### 2.6.3 SplitPanelLayout (๋ถ„ํ•  ํŒจ๋„) + +**ํ˜ธํ™˜์„ฑ**: โš ๏ธ **๋ณ„๋„ ์ˆ˜์ • ํ•„์š”** +- ์™ธ๋ถ€ ์œ„์น˜: ResponsiveGridLayout์ด ๊ด€๋ฆฌ โœ… +- ๋‚ด๋ถ€ ๋ฐ˜์‘ํ˜•: ๋ณ„๋„ ์ˆ˜์ • ํ•„์š” (๋ชจ๋ฐ”์ผ์—์„œ ์ƒํ•˜ ๋ถ„ํ• ) + +#### 2.6.4 ํ˜ธํ™˜์„ฑ ์š”์•ฝ + +| ์ปดํฌ๋„ŒํŠธ | ์™ธ๋ถ€ ๋ฐฐ์น˜ | ๋‚ด๋ถ€ ๋™์ž‘ | ์ถ”๊ฐ€ ์ˆ˜์ • | +|----------|----------|----------|-----------| +| **v2-button, v2-input ๋“ฑ** | โœ… ๋ฐ˜์‘ํ˜• | โœ… shadcn ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | +| **GridLayout** | โœ… ๋ฐ˜์‘ํ˜• | โœ… CSS Grid ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | +| **FlexboxLayout** | โœ… ๋ฐ˜์‘ํ˜• | โš ๏ธ absolute ์œ ์ง€ | โŒ ๋ถˆํ•„์š” | +| **SplitPanelLayout** | โœ… ๋ฐ˜์‘ํ˜• | โŒ ์ขŒ์šฐ ๊ณ ์ • | โš ๏ธ ๋‚ด๋ถ€ ๋ฐ˜์‘ํ˜• ์ถ”๊ฐ€ | +| **TabsLayout** | โœ… ๋ฐ˜์‘ํ˜• | โœ… ํƒญ ๊ทธ๋Œ€๋กœ | โŒ ๋ถˆํ•„์š” | + +### 2.7 ๋™์ž‘ ๋ฐฉ์‹ ๋น„๊ต + +#### ๋ณ€๊ฒฝ ์ „ + +``` +ํ™”๋ฉด ๋กœ๋“œ + โ†“ +screen_layouts_v2์—์„œ components ์กฐํšŒ + โ†“ +๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ position.x, position.y๋กœ absolute ๋ฐฐ์น˜ + โ†“ +GridLayout ์ปดํฌ๋„ŒํŠธ๋„ absolute๋กœ ๋ฐฐ์น˜๋จ + โ†“ +GridLayout ๋‚ด๋ถ€๋Š” CSS Grid๋กœ zone ๋ฐฐ์น˜ + โ†“ +๊ฒฐ๊ณผ: ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€ํ•ด๋„ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๊ณ ์ • +``` + +#### ๋ณ€๊ฒฝ ํ›„ + +``` +ํ™”๋ฉด ๋กœ๋“œ + โ†“ +screen_layouts_v2์—์„œ components ์กฐํšŒ + โ†“ +layoutMode === "grid" ํ™•์ธ + โ†“ +ResponsiveGridLayout์œผ๋กœ ๋ Œ๋”๋ง (CSS Grid) + โ†“ +๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ grid.col, grid.colSpan์œผ๋กœ ๋ฐฐ์น˜ + โ†“ +ํ™”๋ฉด ํฌ๊ธฐ ๊ฐ์ง€ (ResizeObserver) + โ†“ +breakpoint์— ๋”ฐ๋ผ responsive.sm/md/lg ์ ์šฉ + โ†“ +GridLayout ์ปดํฌ๋„ŒํŠธ๋„ ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋ฐฐ์น˜๋จ + โ†“ +GridLayout ๋‚ด๋ถ€๋Š” ๊ธฐ์กด CSS Grid๋กœ zone ๋ฐฐ์น˜ (๋ณ€๊ฒฝ ์—†์Œ) + โ†“ +๊ฒฐ๊ณผ: ํ™”๋ฉด ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ปดํฌ๋„ŒํŠธ ์žฌ๋ฐฐ์น˜ +``` + --- ## 3. ๊ธฐ์ˆ  ๊ฒฐ์ • @@ -649,6 +805,10 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; - [ ] ํƒœ๋ธ”๋ฆฟ (768px, 1024px) ํ…Œ์ŠคํŠธ - [ ] ๋ชจ๋ฐ”์ผ (375px, 414px) ํ…Œ์ŠคํŠธ - [ ] ๋ถ„ํ•  ํŒจ๋„ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] GridLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] FlexboxLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] TabsLayout ์ปดํฌ๋„ŒํŠธ ํฌํ•จ ํ™”๋ฉด ํ…Œ์ŠคํŠธ +- [ ] ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ (GridLayout ์•ˆ์— ์ปดํฌ๋„ŒํŠธ) ํ…Œ์ŠคํŠธ --- @@ -659,6 +819,8 @@ ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2; | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ | ๋†’์Œ | ๋ฐฑ์—… ํ…Œ์ด๋ธ”์—์„œ ์ฆ‰์‹œ ๋กค๋ฐฑ | | ๊ธฐ์กด ํ™”๋ฉด ๊นจ์ง | ์ค‘๊ฐ„ | `layoutMode` ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ (ํด๋ฐฑ) | | ๋””์ž์ธ ๋ชจ๋“œ ํ˜ผ๋ž€ | ๋‚ฎ์Œ | position/size ํ•„๋“œ ์œ ์ง€ | +| GridLayout ๋‚ด๋ถ€ ๊นจ์ง | ๋‚ฎ์Œ | ๋‚ด๋ถ€๋Š” ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€, ์™ธ๋ถ€ ๋ฐฐ์น˜๋งŒ ๋ณ€๊ฒฝ | +| ์ค‘์ฒฉ ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ | ๋‚ฎ์Œ | ๊ฐ ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ | --- diff --git a/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md b/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md new file mode 100644 index 00000000..801fb213 --- /dev/null +++ b/docs/DDD1542/V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md @@ -0,0 +1,399 @@ +# V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ•™์Šต๋…ธํŠธ (DDD1542 ์ „์šฉ) + +> **๋ชฉ์ **: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—… ์ „ ์™„๋ฒฝํ•œ ์ดํ•ด๋ฅผ ์œ„ํ•œ ๊ฐœ์ธ ํ•™์Šต๋…ธํŠธ +> **์ž‘์„ฑ์ผ**: 2026-02-03 +> **์ ˆ๋Œ€ ๊ทœ์น™**: ๋ชจ๋ฅด๋ฉด ๋ฌผ์–ด๋ณด๊ธฐ, ์ถ”์ธก ๊ธˆ์ง€ + +--- + +## 1. ๊ฐ€์žฅ ์ค‘์š”ํ•œ ํ•ต์‹ฌ (์ด์ „ ์‹ ํ•˜๊ฐ€ ์‹คํŒจํ•œ ์ด์œ ) + +### 1.1 "component" vs "v2-input" ์ฐจ์ด + +``` +[์ž˜๋ชป๋œ ์ƒํƒœ] [์˜ฌ๋ฐ”๋ฅธ ์ƒํƒœ] +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ component โ”‚ โ”‚ v2-input โ”‚ +โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ +โ”‚ "์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค" โ”‚ โ”‚ "์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค" โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์Œ table_name + column_name ์—ฐ๊ฒฐ๋จ +``` + +**ํ•ต์‹ฌ**: ์ปฌ๋Ÿผ์„ ์™ผ์ชฝ ํŒจ๋„์—์„œ **๋“œ๋ž˜๊ทธ**ํ•ด์•ผ ์˜ฌ๋ฐ”๋ฅธ ์—ฐ๊ฒฐ์ด ์ƒ์„ฑ๋จ + +### 1.2 ์˜ฌ๋ฐ”๋ฅธ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ๋ฐฉ๋ฒ• + +``` +[์™ผ์ชฝ ํŒจ๋„: ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก] +์šด์†ก์—…์ฒด (8๊ฐœ) +โ”œโ”€โ”€ ์—…์ฒด์ฝ”๋“œ [numbering] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-numbering-rule (๋˜๋Š” v2-input) +โ”œโ”€โ”€ ์—…์ฒด๋ช… [text] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-input +โ”œโ”€โ”€ ์œ ํ˜• [category] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-select +โ”œโ”€โ”€ ์—ฐ๋ฝ์ฒ˜ [text] โ”€๋“œ๋ž˜๊ทธโ†’ ํ™”๋ฉด ์บ”๋ฒ„์Šค โ†’ v2-input +โ””โ”€โ”€ ... +``` + +### 1.3 input_type โ†’ V2 ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| table_type_columns.input_type | V2 ์ปดํฌ๋„ŒํŠธ | ์—ฐ๋™ ํ…Œ์ด๋ธ” | +|-------------------------------|-------------|-------------| +| text | v2-input | - | +| number | v2-input (type=number) | - | +| date | v2-date | - | +| category | v2-select | category_values | +| numbering | v2-numbering-rule ๋˜๋Š” v2-input | numbering_rules | +| entity | v2-entity-search | ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ | + +--- + +## 2. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +### 2.1 ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + +``` +V1 (๋ณธ์„œ๋ฒ„: screen_layouts) V2 (๊ฐœ๋ฐœ์„œ๋ฒ„: screen_layouts_v2) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +- ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ ๋ ˆ์ฝ”๋“œ - ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +- properties JSONB - layout_data JSONB +- component_type VARCHAR - url (์ปดํฌ๋„ŒํŠธ ๊ฒฝ๋กœ) +- menu_objid ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ - table_name + column_name ๊ธฐ๋ฐ˜ +``` + +### 2.2 V2 layout_data ๊ตฌ์กฐ + +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "inspection_standard", + "columns": ["id", "name", "status"] + } + } + ], + "updatedAt": "2026-02-03T12:00:00Z" +} +``` + +### 2.3 ์ปดํฌ๋„ŒํŠธ URL ๋งคํ•‘ + +```typescript +const V1_TO_V2_URL_MAPPING = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'split-panel-layout': '@/lib/registry/components/v2-split-panel-layout', +}; +``` + +--- + +## 3. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ (V2) + +### 3.1 ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +table_type_columns (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜) +โ”œโ”€โ”€ input_type = 'category' โ†’ category_values (table_name + column_name) +โ”œโ”€โ”€ input_type = 'numbering' โ†’ numbering_rules (detail_settings.numberingRuleId) +โ”œโ”€โ”€ input_type = 'entity' โ†’ ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ +โ””โ”€โ”€ input_type = 'text', 'number', 'date', etc. +``` + +### 3.2 category_values ์กฐํšŒ ์ฟผ๋ฆฌ + +```sql +-- ํŠน์ • ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +SELECT value_id, value_code, value_label, parent_value_id, depth +FROM category_values +WHERE table_name = 'ํ…Œ์ด๋ธ”๋ช…' + AND column_name = '์ปฌ๋Ÿผ๋ช…' + AND company_code = 'COMPANY_7' +ORDER BY value_order; +``` + +### 3.3 numbering_rules ์—ฐ๊ฒฐ ๋ฐฉ์‹ + +```json +// table_type_columns.detail_settings +{ + "numberingRuleId": "rule-xxx" +} + +// numbering_rules์—์„œ ํ•ด๋‹น rule ์กฐํšŒ +SELECT * FROM numbering_rules WHERE rule_id = 'rule-xxx'; +``` + +--- + +## 4. V2 ์ปดํฌ๋„ŒํŠธ ๋ชฉ๋ก (23๊ฐœ) + +### 4.1 ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-input | ์ž…๋ ฅ | ํ…์ŠคํŠธ, ์ˆซ์ž, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ | +| v2-select | ์„ ํƒ | ๋“œ๋กญ๋‹ค์šด, ๋ผ๋””์˜ค, ์ฒดํฌ๋ฐ•์Šค | +| v2-date | ๋‚ ์งœ | ๋‚ ์งœ, ์‹œ๊ฐ„, ๋‚ ์งœ๋ฒ”์œ„ | + +### 4.2 ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-text-display | ํ…์ŠคํŠธ ํ‘œ์‹œ | ๋ผ๋ฒจ, ์ œ๋ชฉ | +| v2-card-display | ์นด๋“œ ๋””์Šคํ”Œ๋ ˆ์ด | ์นด๋“œ ํ˜•ํƒœ ๋ฐ์ดํ„ฐ | +| v2-aggregation-widget | ์ง‘๊ณ„ ์œ„์ ฏ | ํ•ฉ๊ณ„, ํ‰๊ท , ๊ฐœ์ˆ˜ | + +### 4.3 ํ…Œ์ด๋ธ”/๋ฐ์ดํ„ฐ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-table-list | ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ | ๋ฐ์ดํ„ฐ ๊ทธ๋ฆฌ๋“œ | +| v2-table-search-widget | ๊ฒ€์ƒ‰ ํ•„ํ„ฐ | ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰ | +| v2-pivot-grid | ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ | ๋‹ค์ฐจ์› ๋ถ„์„ | +| v2-table-grouped | ๊ทธ๋ฃนํ™” ํ…Œ์ด๋ธ” | ๊ทธ๋ฃน๋ณ„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ | + +### 4.4 ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-split-panel-layout | ๋ถ„ํ•  ํŒจ๋„ | ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ | +| v2-tabs-widget | ํƒญ ์œ„์ ฏ | ํƒญ ์ „ํ™˜ | +| v2-section-card | ์„น์…˜ ์นด๋“œ | ์ œ๋ชฉ+ํ…Œ๋‘๋ฆฌ ๊ทธ๋ฃน | +| v2-section-paper | ์„น์…˜ ํŽ˜์ดํผ | ๋ฐฐ๊ฒฝ์ƒ‰ ๊ทธ๋ฃน | +| v2-divider-line | ๊ตฌ๋ถ„์„  | ์˜์—ญ ๊ตฌ๋ถ„ | +| v2-repeat-container | ๋ฆฌํ”ผํ„ฐ ์ปจํ…Œ์ด๋„ˆ | ๋ฐ์ดํ„ฐ ๋ฐ˜๋ณต | +| v2-unified-repeater | ํ†ตํ•ฉ ๋ฆฌํ”ผํ„ฐ | ์ธ๋ผ์ธ/๋ชจ๋‹ฌ/๋ฒ„ํŠผ | + +### 4.5 ์•ก์…˜/ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ + +| ID | ์ด๋ฆ„ | ์šฉ๋„ | +|----|------|------| +| v2-button-primary | ๊ธฐ๋ณธ ๋ฒ„ํŠผ | ์ €์žฅ, ์‚ญ์ œ ๋“ฑ | +| v2-numbering-rule | ์ฑ„๋ฒˆ ๊ทœ์น™ | ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ | +| v2-category-manager | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ์ž | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ | +| v2-location-swap-selector | ์œ„์น˜ ๊ตํ™˜ | ์œ„์น˜ ์„ ํƒ | +| v2-rack-structure | ๋ž™ ๊ตฌ์กฐ | ์ฐฝ๊ณ  ๋ž™ ์‹œ๊ฐํ™” | + +--- + +## 5. ํ™”๋ฉด ํŒจํ„ด (5๊ฐ€์ง€) + +### 5.1 ํŒจํ„ด A: ๊ธฐ๋ณธ ๋งˆ์Šคํ„ฐ ํ™”๋ฉด + +``` +์‚ฌ์šฉ ์กฐ๊ฑด: ๋‹จ์ผ ํ…Œ์ด๋ธ” CRUD + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ v2-table-search-widget โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ v2-table-list โ”‚ +โ”‚ [์‹ ๊ทœ] [์‚ญ์ œ] v2-button-primary โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 5.2 ํŒจํ„ด B: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ํ™”๋ฉด + +``` +์‚ฌ์šฉ ์กฐ๊ฑด: ๋งˆ์Šคํ„ฐ ์„ ํƒ โ†’ ๋””ํ…Œ์ผ ํ‘œ์‹œ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ ๋””ํ…Œ์ผ ๋ฆฌ์ŠคํŠธ โ”‚ +โ”‚ v2-table-list โ”‚ v2-table-list โ”‚ +โ”‚ โ”‚ (relation: foreignKey) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +**ํ•„์ˆ˜ ์„ค์ •:** +```json +{ + "leftPanel": { "tableName": "master_table" }, + "rightPanel": { + "tableName": "detail_table", + "relation": { "type": "detail", "foreignKey": "master_id" } + }, + "splitRatio": 30 +} +``` + +### 5.3 ํŒจํ„ด C: ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ + ํƒญ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ โ”‚ v2-tabs-widget โ”‚ +โ”‚ v2-table-list โ”‚ โ”œโ”€ ํƒญ1: v2-table-list โ”‚ +โ”‚ โ”‚ โ”œโ”€ ํƒญ2: v2-table-list โ”‚ +โ”‚ โ”‚ โ””โ”€ ํƒญ3: ํผ ์ปดํฌ๋„ŒํŠธ๋“ค โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + v2-split-panel-layout +``` + +--- + +## 6. ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +### 6.1 V1 (๋ณธ์„œ๋ฒ„) + +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ™”๋ฉด B (screen_id: 143) - ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ (๋ณ„๋„ screen_id) +``` + +### 6.2 V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) + +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ layout_data.components[] ๋‚ด์— v2-dialog-form ๋˜๋Š” overlay ํฌํ•จ +``` + +**ํ•ต์‹ฌ**: V2์—์„œ๋Š” ๋ชจ๋‹ฌ์„ ๋ณ„๋„ ํ™”๋ฉด์ด ์•„๋‹Œ, ๋ถ€๋ชจ ํ™”๋ฉด์˜ ์ปดํฌ๋„ŒํŠธ๋กœ ํ†ตํ•ฉ + +--- + +## 7. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ (Step by Step) + +### Step 1: ์‚ฌ์ „ ๋ถ„์„ + +```sql +-- ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_code LIKE 'COMPANY_7_%' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ˜„ํ™ฉ ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_data IS NOT NULL as has_v2_layout +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### Step 2: table_type_columns ํ™•์ธ + +```sql +-- ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ํƒ€์ž… ํ™•์ธ +SELECT column_name, column_label, input_type, detail_settings +FROM table_type_columns +WHERE table_name = '๋Œ€์ƒํ…Œ์ด๋ธ”๋ช…' + AND company_code = 'COMPANY_7'; +``` + +### Step 3: V2 layout_data ์ƒ์„ฑ + +```json +{ + "version": "2.0", + "components": [ + { + "id": "์ƒ์„ฑ๋œID", + "url": "@/lib/registry/components/v2-์ปดํฌ๋„ŒํŠธํƒ€์ž…", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "ํ…Œ์ด๋ธ”๋ช…", + "fieldName": "์ปฌ๋Ÿผ๋ช…" + } + } + ], + "migratedFrom": "V1", + "migratedAt": "2026-02-03T00:00:00Z" +} +``` + +### Step 4: screen_layouts_v2 INSERT + +```sql +INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data) +VALUES ($1, $2, $3::jsonb) +ON CONFLICT (screen_id, company_code) +DO UPDATE SET layout_data = $3::jsonb, updated_at = NOW(); +``` + +### Step 5: ๊ฒ€์ฆ + +- [ ] ํ™”๋ฉด ๋ Œ๋”๋ง ํ™•์ธ (component๊ฐ€ ์•„๋‹Œ v2-xxx๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€) +- [ ] ์ปดํฌ๋„ŒํŠธ๋ณ„ ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ํ™•์ธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ ํ™•์ธ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ํ…Œ์ŠคํŠธ + +--- + +## 8. ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ˜„ํ™ฉ + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ํ…Œ์ด๋ธ” | ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|--------|------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | inspection_standard | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ฆ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | - | โœ… V2 ์กด์žฌ | v2-category-manager | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | โ†’ 142 ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | defect_standard_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | defect_standard_mng | โŒ ๋ˆ„๋ฝ | โ†’ 144 ํ†ตํ•ฉ | + +--- + +## 9. ๊ด€๋ จ ์ฝ”๋“œ ํŒŒ์ผ ๊ฒฝ๋กœ + +| ํ•ญ๋ชฉ | ๊ฒฝ๋กœ | +|------|------| +| V2 ์ปดํฌ๋„ŒํŠธ ํด๋” | `frontend/lib/registry/components/v2-xxx/` | +| ์ปดํฌ๋„ŒํŠธ ๋“ฑ๋ก | `frontend/lib/registry/components/index.ts` | +| ์นดํ…Œ๊ณ ๋ฆฌ ์„œ๋น„์Šค | `backend-node/src/services/categoryTreeService.ts` | +| ์ฑ„๋ฒˆ ์„œ๋น„์Šค | `backend-node/src/services/numberingRuleService.ts` | +| ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API | `frontend/lib/api/entityJoin.ts` | +| ํผ ํ˜ธํ™˜์„ฑ ํ›… | `frontend/hooks/useFormCompatibility.ts` | + +--- + +## 10. ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ + +1. โŒ **ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์ด ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜** โ†’ "component"๋กœ ํ‘œ์‹œ๋จ +2. โŒ **menu_objid ๊ธฐ๋ฐ˜ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์‚ฌ์šฉ** โ†’ V2๋Š” table_name + column_name ๊ธฐ๋ฐ˜ +3. โŒ **๋ชจ๋‹ฌ์„ ๋ณ„๋„ screen_id๋กœ ์ƒ์„ฑ** โ†’ V2๋Š” ๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ +4. โŒ **V1 ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ์‚ฌ์šฉ** โ†’ ๋ฐ˜๋“œ์‹œ v2- ์ ‘๋‘์‚ฌ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ +5. โŒ **company_code ํ•„ํ„ฐ๋ง ๋ˆ„๋ฝ** โ†’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜ + +--- + +## 11. ๋ชจ๋ฅด๋ฉด ํ™•์ธํ•  ๊ณณ + +1. **์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ**: `docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md` +2. **ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€**: `docs/screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md` +3. **๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ**: `docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md` +4. **ํƒ‘์‹ค ๋””์ž์ธ ๋ช…์„ธ**: `/Users/gbpark/Downloads/ํ™”๋ฉด๊ฐœ๋ฐœ 8/` +5. **์‹ค์ œ ์ฝ”๋“œ**: ์œ„ ๊ฒฝ๋กœ์˜ ์†Œ์Šค ํŒŒ์ผ๋“ค + +--- + +## 12. ์™•์˜ ํ›ˆ๊ณ„ + +> **"ํ•ญ์ƒ ์• ๋งคํ•œ ๊ฑฐ๋Š” mdํŒŒ์ผ ๋ณด๊ฑฐ๋‚˜ ๋ฌผ์–ด๋ณผ ๊ฒƒ. ์ฝ”๋“œ์—๋Š” ์ „๋ถ€ ์ •๋‹ต์ด ์žˆ์Œ. ๋งŒ์•ฝ ๋ชจ๋ฅธ๋‹ค๋ฉด ๋„ˆ ์ž˜๋ชป. ์‹ค์ˆ˜ํ•ด๋„ ๋„ˆ ์ž˜๋ชป."** + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ (๋ฌธ์„œ 4๊ฐœ ์ •๋… ํ›„) | diff --git a/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..b7a0e353 --- /dev/null +++ b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md @@ -0,0 +1,453 @@ +# ๋ณธ์„œ๋ฒ„ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ (๊ณต์šฉ) + +> **์ด ๋ฌธ์„œ๋Š” ๋‹ค์Œ AI ์—์ด์ „ํŠธ๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—…์„ ์ด์–ด๋ฐ›์„ ๋•Œ ์ฐธ๊ณ ํ•˜๋Š” ํ•ต์‹ฌ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.** + +--- + +## ๋น ๋ฅธ ์‹œ์ž‘ + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉํ–ฅ (์ ˆ๋Œ€ ์žŠ์ง€ ๋ง ๊ฒƒ) + +``` +๋ณธ์„œ๋ฒ„ (Production) โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +211.115.91.141:11134 39.117.244.52:11132 +screen_layouts (V1) screen_layouts_v2 (V2) +``` + +**๋ฐ˜๋Œ€๋กœ ํ•˜๋ฉด ์•ˆ ๋จ!** ๊ฐœ๋ฐœ์„œ๋ฒ„ ์™„์„ฑ ํ›„ โ†’ ๋ณธ์„œ๋ฒ„๋กœ ๋ฐฐํฌ ์˜ˆ์ • + +### DB ์ ‘์† ์ •๋ณด + +```bash +# ๋ณธ์„œ๋ฒ„ (Production) +docker exec pms-backend-mac node -e ' +const { Pool } = require("pg"); +const pool = new Pool({ + connectionString: "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/plm?sslmode=disable", + ssl: false +}); +// ์ฟผ๋ฆฌ ์‹คํ–‰ +' + +# ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +docker exec pms-backend-mac node -e ' +const { Pool } = require("pg"); +const pool = new Pool({ + connectionString: "postgresql://postgres:ph0909!!@39.117.244.52:11132/plm?sslmode=disable", + ssl: false +}); +// ์ฟผ๋ฆฌ ์‹คํ–‰ +' +``` + +--- + +## ์ ˆ๋Œ€ ์ฃผ์˜: ์ปดํฌ๋„ŒํŠธ-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ (์ด์ „ ์‹คํŒจ ์›์ธ) + +### "component" vs "v2-input" ๊ตฌ๋ถ„ + +``` +โŒ ์ž˜๋ชป๋œ ์ƒํƒœ โœ… ์˜ฌ๋ฐ”๋ฅธ ์ƒํƒœ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ component โ”‚ โ”‚ v2-input โ”‚ +โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ โ”‚ ์—…์ฒด์ฝ”๋“œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†‘ โ†‘ + overrides.type ์—†์Œ overrides.type = "v2-input" +``` + +**ํ•ต์‹ฌ ์›์ธ**: ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ทธ๋ƒฅ ๋ฐฐ์น˜ํ•˜๋ฉด "component"๋กœ ํ‘œ์‹œ๋จ. ๋ฐ˜๋“œ์‹œ ์™ผ์ชฝ ํŒจ๋„์—์„œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ **๋“œ๋ž˜๊ทธ**ํ•ด์•ผ ์˜ฌ๋ฐ”๋ฅธ v2-xxx ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์„ฑ๋จ. + +### ๐Ÿ”ฅ ํ•ต์‹ฌ ๋ฐœ๊ฒฌ: overrides.type ํ•„์ˆ˜ (2026-02-04 ๋ฐœ๊ฒฌ) + +**"component"๋กœ ํ‘œ์‹œ๋˜๋Š” ๊ทผ๋ณธ ์›์ธ:** + +| ํ•ญ๋ชฉ | ๋“œ๋ž˜๊ทธ๋กœ ๋ฐฐ์น˜ | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์ž˜๋ชป๋œ) | +|------|---------------|----------------------| +| `overrides.type` | **"v2-input"** โœ… | **์—†์Œ** โŒ | +| `overrides.webType` | "text" ๋“ฑ | ์—†์Œ | +| `overrides.tableName` | "carrier_mng" ๋“ฑ | ์—†์Œ | + +**ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์„ ์ธ์‹ํ•˜๋Š” ๋ฐฉ๋ฒ•:** +1. `overrides.type` ํ™•์ธ โ†’ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๊ฐ’ ์‚ฌ์šฉ (์˜ˆ: "v2-input") +2. ์—†์œผ๋ฉด โ†’ ๊ธฐ๋ณธ๊ฐ’ "component"๋กœ ํด๋ฐฑ + +**๊ฒฐ๋ก **: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ `overrides.type` ํ•„๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ค์ •ํ•ด์•ผ ํ•จ! + +### input_type โ†’ V2 ์ปดํฌ๋„ŒํŠธ ์ž๋™ ๋งคํ•‘ + +| table_type_columns.input_type | ๋“œ๋ž˜๊ทธ ์‹œ ์ƒ์„ฑ๋˜๋Š” V2 ์ปดํฌ๋„ŒํŠธ | +|-------------------------------|-------------------------------| +| text | v2-input | +| number | v2-input (type=number) | +| date | v2-date | +| category | v2-select (category_values ์—ฐ๋™) | +| numbering | v2-numbering-rule ๋˜๋Š” v2-input | +| entity | v2-entity-search | + +**์ ˆ๋Œ€ ๊ทœ์น™**: ์ปดํฌ๋„ŒํŠธ๊ฐ€ "component"๋กœ ํ‘œ์‹œ๋˜๋ฉด ์—ฐ๊ฒฐ ์‹คํŒจ ์ƒํƒœ. ๋ฐ˜๋“œ์‹œ "v2-xxx"๋กœ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•จ. + +--- + +## ํ•ต์‹ฌ ๊ฐœ๋… + +### V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +| ๊ตฌ๋ถ„ | V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | +|------|-------------|---------------| +| ํ…Œ์ด๋ธ” | screen_layouts | screen_layouts_v2 | +| ๋ ˆ์ฝ”๋“œ | ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ | ํ™”๋ฉด๋‹น 1๊ฐœ | +| ์„ค์ • ์ €์žฅ | properties JSONB | layout_data.components[].overrides | +| ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ | menu_objid ๊ธฐ๋ฐ˜ | table_name + column_name ๊ธฐ๋ฐ˜ | +| ์ปดํฌ๋„ŒํŠธ ์ฐธ์กฐ | component_type ๋ฌธ์ž์—ด | url ๊ฒฝ๋กœ (@/lib/registry/...) | + +### ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ (V2) + +``` +table_type_columns (input_type) +โ”œโ”€โ”€ 'category' โ†’ category_values ํ…Œ์ด๋ธ” +โ”œโ”€โ”€ 'numbering' โ†’ numbering_rules ํ…Œ์ด๋ธ” (detail_settings.numberingRuleId) +โ”œโ”€โ”€ 'entity' โ†’ ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ +โ””โ”€โ”€ 'text', 'number', 'date', etc. +``` + +### ์ปดํฌ๋„ŒํŠธ URL ๋งคํ•‘ + +```typescript +const V1_TO_V2_MAPPING = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-text-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date-input', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'textarea-basic': '@/lib/registry/components/v2-textarea', +}; +``` + +### ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +- **V1**: ๋ณ„๋„ ํ™”๋ฉด(screen_id)์œผ๋กœ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ +- **V2**: ๋ถ€๋ชจ ํ™”๋ฉด์— overlay/dialog ์ปดํฌ๋„ŒํŠธ๋กœ ํ†ตํ•ฉ + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ ๋ฉ”๋‰ด ํ˜„ํ™ฉ + +### ํ’ˆ์งˆ๊ด€๋ฆฌ (์šฐ์„ ์ˆœ์œ„ 1) + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ฆ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | โœ… V2 ์กด์žฌ | v2-category-manager ์‚ฌ์šฉ์ค‘ | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | โŒ ๋ˆ„๋ฝ | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | โŒ ๋ˆ„๋ฝ | โ†’ 142์— ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | โŒ ๋ˆ„๋ฝ | โ†’ 144์— ํ†ตํ•ฉ | + +### ๋‹ค์Œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ (๋ฏธ์ •) + +- [ ] ๋ฌผ๋ฅ˜๊ด€๋ฆฌ +- [ ] ์ƒ์‚ฐ๊ด€๋ฆฌ +- [ ] ์˜์—…๊ด€๋ฆฌ +- [ ] ๊ธฐํƒ€ ๋ฉ”๋‰ด๋“ค + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ž‘์—… ์ ˆ์ฐจ + +### Step 1: ๋ถ„์„ + +```sql +-- ๋ณธ์„œ๋ฒ„ ํŠน์ • ๋ฉ”๋‰ด ํ™”๋ฉด ๋ชฉ๋ก ์กฐํšŒ +SELECT + sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_name LIKE '%[๋ฉ”๋‰ด๋ช…]%' + AND sd.company_code = 'COMPANY_7' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ˜„ํ™ฉ ํ™•์ธ +SELECT + sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_id IS NOT NULL as has_v2 +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### Step 2: screen_definitions ๋™๊ธฐํ™” + +๋ณธ์„œ๋ฒ„์—๋งŒ ์žˆ๋Š” ํ™”๋ฉด์„ ๊ฐœ๋ฐœ์„œ๋ฒ„์— ์ถ”๊ฐ€ + +### Step 3: V1 โ†’ V2 ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™˜ + +```typescript +// layout_data ๊ตฌ์กฐ +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "ํ…Œ์ด๋ธ”๋ช…", + "columns": ["์ปฌ๋Ÿผ1", "์ปฌ๋Ÿผ2"] + } + } + ] +} +``` + +### Step 4: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ ํ™•์ธ/์ƒ์„ฑ + +```sql +-- ํ…Œ์ด๋ธ”์˜ category ์ปฌ๋Ÿผ ํ™•์ธ +SELECT column_name, column_label +FROM table_type_columns +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND input_type = 'category'; + +-- category_values ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT value_id, value_code, value_label +FROM category_values +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND column_name = '[์ปฌ๋Ÿผ๋ช…]' + AND company_code = 'COMPANY_7'; +``` + +### Step 5: ์ฑ„๋ฒˆ ๊ทœ์น™ ํ™•์ธ/์ƒ์„ฑ + +```sql +-- numbering ์ปฌ๋Ÿผ ํ™•์ธ +SELECT column_name, column_label, detail_settings +FROM table_type_columns +WHERE table_name = '[ํ…Œ์ด๋ธ”๋ช…]' + AND input_type = 'numbering'; + +-- numbering_rules ๋ฐ์ดํ„ฐ ํ™•์ธ +SELECT rule_id, rule_name, table_name, column_name +FROM numbering_rules +WHERE company_code = 'COMPANY_7'; +``` + +### Step 6: ๊ฒ€์ฆ + +- [ ] ํ™”๋ฉด ๋ Œ๋”๋ง ํ™•์ธ +- [ ] ์ปดํฌ๋„ŒํŠธ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ํ…Œ์ŠคํŠธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ + +--- + +## ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ + +### screen_layouts_v2 + +```sql +CREATE TABLE screen_layouts_v2 ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER NOT NULL, + company_code VARCHAR(20) NOT NULL, + layout_data JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(screen_id, company_code) +); +``` + +### category_values + +```sql +-- ํ•ต์‹ฌ ์ปฌ๋Ÿผ +value_id, table_name, column_name, value_code, value_label, +parent_value_id, depth, path, company_code +``` + +### numbering_rules + numbering_rule_parts + +```sql +-- numbering_rules ํ•ต์‹ฌ ์ปฌ๋Ÿผ +rule_id, rule_name, table_name, column_name, separator, +reset_period, current_sequence, company_code + +-- numbering_rule_parts ํ•ต์‹ฌ ์ปฌ๋Ÿผ +rule_id, part_order, part_type, generation_method, +auto_config, manual_config, company_code +``` + +### table_type_columns + +```sql +-- ํ•ต์‹ฌ ์ปฌ๋Ÿผ +table_name, column_name, input_type, column_label, +detail_settings, company_code +``` + +--- + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +### ํ•„์ˆ˜ ์ฝ๊ธฐ + +1. **[๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md](./๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md)** - ์ƒ์„ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ +2. **[ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md](../screen-implementation-guide/ํ™”๋ฉด๊ฐœ๋ฐœ_ํ‘œ์ค€_๊ฐ€์ด๋“œ.md)** - V2 ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€ +3. **[SCREEN_DEVELOPMENT_STANDARD.md](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md)** - ์˜๋ฌธ ํ‘œ์ค€ ๊ฐ€์ด๋“œ + +### ์ฝ”๋“œ ์ฐธ์กฐ + +| ํŒŒ์ผ | ์„ค๋ช… | +|------|------| +| `backend-node/src/services/categoryTreeService.ts` | ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ ์„œ๋น„์Šค | +| `backend-node/src/services/numberingRuleService.ts` | ์ฑ„๋ฒˆ ๊ทœ์น™ ์„œ๋น„์Šค | +| `frontend/lib/registry/components/v2-category-manager/` | V2 ์นดํ…Œ๊ณ ๋ฆฌ ์ปดํฌ๋„ŒํŠธ | +| `frontend/lib/registry/components/v2-numbering-rule/` | V2 ์ฑ„๋ฒˆ ์ปดํฌ๋„ŒํŠธ | + +### ๊ด€๋ จ ๋ฌธ์„œ + +- `docs/V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md` +- `docs/V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md` +- `docs/DDD1542/COMPONENT_LAYOUT_V2_ARCHITECTURE.md` +- `docs/DDD1542/COMPONENT_MIGRATION_PLAN.md` + +--- + +## ์ฃผ์˜์‚ฌํ•ญ + +### ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ + +1. **๊ฐœ๋ฐœ์„œ๋ฒ„ โ†’ ๋ณธ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜** (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ) +2. **๋ณธ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ ์ง์ ‘ ์ˆ˜์ •** (SELECT๋งŒ ํ—ˆ์šฉ) +3. **company_code ๋ˆ„๋ฝ** (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„์ˆ˜) +4. **ํ…Œ์ด๋ธ”-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์—†์ด ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜** ("component"๋กœ ํ‘œ์‹œ๋˜๋ฉด ์‹คํŒจ) +5. **menu_objid ๊ธฐ๋ฐ˜ ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์‚ฌ์šฉ** (V2๋Š” table_name + column_name ๊ธฐ๋ฐ˜) + +### ๋ฐ˜๋“œ์‹œ ํ•  ๊ฒƒ + +1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ **๊ฐœ๋ฐœ์„œ๋ฒ„ ๋ฐฑ์—…** +2. ์ปดํฌ๋„ŒํŠธ ๋ณ€ํ™˜ ์‹œ **V2 ์ปดํฌ๋„ŒํŠธ๋งŒ ์‚ฌ์šฉ** (v2- prefix) +3. ๋ชจ๋‹ฌ ํ™”๋ฉด์€ **๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ** +4. ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ์€ **table_name + column_name ๊ธฐ๋ฐ˜** +5. ์ปดํฌ๋„ŒํŠธ ๋ฐฐ์น˜ ํ›„ **"v2-xxx"๋กœ ํ‘œ์‹œ๋˜๋Š”์ง€ ๋ฐ˜๋“œ์‹œ ํ™•์ธ** + +### ์‹คํŒจ ์‚ฌ๋ก€ (์ด์ „ ์ž‘์—…์ž) + +**๋ฌผ๋ฅ˜์ •๋ณด๊ด€๋ฆฌ โ†’ ์šด์†ก์—…์ฒด ๊ด€๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ** + +- **์›์ธ**: ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ๋ฐฐ์น˜ํ•˜์—ฌ "component"๋กœ ์ƒ์„ฑ๋จ +- **์ฆ์ƒ**: ํ™”๋ฉด์— "component" ๋ผ๋ฒจ ํ‘œ์‹œ, ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ +- **ํ•ด๊ฒฐ**: ์™ผ์ชฝ ํŒจ๋„์—์„œ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ๋“œ๋ž˜๊ทธํ•˜์—ฌ "v2-input" ๋“ฑ์œผ๋กœ ์ƒ์„ฑ + +--- + +## ๐Ÿ”ง ์ผ๊ด„ ์ˆ˜์ • SQL (overrides.type ๋ˆ„๋ฝ ๋ฌธ์ œ) + +### ๋ฌธ์ œ ์ง„๋‹จ ์ฟผ๋ฆฌ + +```sql +-- overrides.type์ด ์—†๋Š” ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ํ™•์ธ +SELECT + COUNT(DISTINCT sv2.screen_id) as affected_screens, + COUNT(*) as affected_components +FROM screen_layouts_v2 sv2, + jsonb_array_elements(sv2.layout_data->'components') as comp +WHERE (comp->>'url' LIKE '%/v2-input' + OR comp->>'url' LIKE '%/v2-select' + OR comp->>'url' LIKE '%/v2-date') + AND NOT (comp->'overrides' ? 'type'); +``` + +### ์ผ๊ด„ ์ˆ˜์ • ์ฟผ๋ฆฌ (๊ฐœ๋ฐœ์„œ๋ฒ„์—์„œ๋งŒ!) + +```sql +UPDATE screen_layouts_v2 +SET layout_data = jsonb_set( + layout_data, + '{components}', + ( + SELECT jsonb_agg( + CASE + WHEN comp->>'url' LIKE '%/v2-input' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-input"') + WHEN comp->>'url' LIKE '%/v2-select' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-select"') + WHEN comp->>'url' LIKE '%/v2-date' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-date"') + WHEN comp->>'url' LIKE '%/v2-textarea' AND NOT (comp->'overrides' ? 'type') + THEN jsonb_set(comp, '{overrides,type}', '"v2-textarea"') + ELSE comp + END + ) + FROM jsonb_array_elements(layout_data->'components') comp + ) +), +updated_at = NOW() +WHERE EXISTS ( + SELECT 1 FROM jsonb_array_elements(layout_data->'components') c + WHERE (c->>'url' LIKE '%/v2-input' OR c->>'url' LIKE '%/v2-select' + OR c->>'url' LIKE '%/v2-date' OR c->>'url' LIKE '%/v2-textarea') + AND NOT (c->'overrides' ? 'type') +); +``` + +### 2026-02-04 ์ผ๊ด„ ์ˆ˜์ • ์‹คํ–‰ ๊ฒฐ๊ณผ + +| ํ•ญ๋ชฉ | ์ˆ˜๋Ÿ‰ | +|------|------| +| ์ˆ˜์ •๋œ ํ™”๋ฉด | 397๊ฐœ | +| ์ˆ˜์ •๋œ ์ปดํฌ๋„ŒํŠธ | 2,455๊ฐœ | +| v2-input | 1,983๊ฐœ | +| v2-select | 336๊ฐœ | +| v2-date | 136๊ฐœ | + +--- + +## ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง„ํ–‰ ๋กœ๊ทธ + +| ๋‚ ์งœ | ๋ฉ”๋‰ด | ๋‹ด๋‹น | ์ƒํƒœ | ๋น„๊ณ  | +|------|------|------|------|------| +| 2026-02-03 | ํ’ˆ์งˆ๊ด€๋ฆฌ | DDD1542 | ๋ถ„์„ ์™„๋ฃŒ | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€๊ธฐ | +| 2026-02-03 | ๋ฌผ๋ฅ˜๊ด€๋ฆฌ (์šด์†ก์—…์ฒด) | ์ด์ „ ์‹ ํ•˜ | โŒ ์‹คํŒจ | component ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ | +| 2026-02-03 | ๋ฌธ์„œ ํ•™์Šต | DDD1542 | โœ… ์™„๋ฃŒ | ํ•ต์‹ฌ 4๊ฐœ ๋ฌธ์„œ ์ •๋…, ํ•™์Šต๋…ธํŠธ ์ž‘์„ฑ | +| **2026-02-04** | **overrides.type ์›์ธ ๋ถ„์„** | **AI** | **โœ… ์™„๋ฃŒ** | **ํ•ต์‹ฌ ์›์ธ ๋ฐœ๊ฒฌ: overrides.type ๋ˆ„๋ฝ** | +| **2026-02-04** | **์ „์ฒด ์ž…๋ ฅํผ ์ผ๊ด„ ์ˆ˜์ •** | **AI** | **โœ… ์™„๋ฃŒ** | **397๊ฐœ ํ™”๋ฉด, 2,455๊ฐœ ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •** | +| | ๋ฌผ๋ฅ˜๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | +| | ์ƒ์‚ฐ๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | +| | ์˜์—…๊ด€๋ฆฌ | - | ๋ฏธ์‹œ์ž‘ | | + +--- + +## ๋‹ค์Œ ์ž‘์—… ์š”์ฒญ ์˜ˆ์‹œ + +๋‹ค์Œ AI์—๊ฒŒ ์š”์ฒญํ•  ๋•Œ ์ด๋ ‡๊ฒŒ ๋งํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค: + +``` +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md ์ฝ๊ณ  ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ง„ํ–‰ํ•ด์ค˜" + +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_๊ฐ€์ด๋“œ.md ์ฐธ๊ณ ํ•ด์„œ ๋ฌผ๋ฅ˜๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋ถ„์„ํ•ด์ค˜" + +"๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md ๋ณด๊ณ  COMPANY_7_142 ํ™”๋ฉด V2๋กœ ๋ณ€ํ™˜ํ•ด์ค˜" +``` + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ | +| 2026-02-03 | DDD1542 | ์ปดํฌ๋„ŒํŠธ-์ปฌ๋Ÿผ ์—ฐ๊ฒฐ ์ฃผ์˜์‚ฌํ•ญ ์ถ”๊ฐ€ (์ด์ „ ์‹คํŒจ ์›์ธ) | +| 2026-02-03 | DDD1542 | ๊ฐœ์ธ ํ•™์Šต๋…ธํŠธ ์ž‘์„ฑ (V2_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_ํ•™์Šต๋…ธํŠธ_DDD1542.md) | +| **2026-02-04** | **AI** | **ํ•ต์‹ฌ ์›์ธ ๋ฐœ๊ฒฌ: overrides.type ํ•„๋“œ ๋ˆ„๋ฝ ๋ฌธ์ œ** | +| **2026-02-04** | **AI** | **์ผ๊ด„ ์ˆ˜์ • SQL ์ถ”๊ฐ€ ๋ฐ 397๊ฐœ ํ™”๋ฉด ์ˆ˜์ • ์™„๋ฃŒ** | diff --git a/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md new file mode 100644 index 00000000..42ce37f1 --- /dev/null +++ b/docs/DDD1542/๋ณธ์„œ๋ฒ„_๊ฐœ๋ฐœ์„œ๋ฒ„_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์ƒ์„ธ๊ฐ€์ด๋“œ.md @@ -0,0 +1,553 @@ +# ๋ณธ์„œ๋ฒ„ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ + +## ๊ฐœ์š” + +๋ณธ ๋ฌธ์„œ๋Š” **๋ณธ์„œ๋ฒ„(Production)**์˜ `screen_layouts` (V1) ๋ฐ์ดํ„ฐ๋ฅผ **๊ฐœ๋ฐœ์„œ๋ฒ„(Development)**์˜ `screen_layouts_v2` ์‹œ์Šคํ…œ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +### ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉํ–ฅ +``` +๋ณธ์„œ๋ฒ„ (Production) ๊ฐœ๋ฐœ์„œ๋ฒ„ (Development) +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ screen_layouts (V1) โ”‚ โ†’ โ”‚ screen_layouts_v2 โ”‚ +โ”‚ - ์ปดํฌ๋„ŒํŠธ๋ณ„ ๋ ˆ์ฝ”๋“œ โ”‚ โ”‚ - ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ โ”‚ +โ”‚ - properties JSONB โ”‚ โ”‚ - layout_data JSONB โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ์ตœ์ข… ๋ชฉํ‘œ +๊ฐœ๋ฐœ์„œ๋ฒ„์—์„œ ์™„์„ฑ ํ›„ **๊ฐœ๋ฐœ์„œ๋ฒ„ โ†’ ๋ณธ์„œ๋ฒ„**๋กœ ๋ฐฐํฌ + +--- + +## 1. V1 vs V2 ๊ตฌ์กฐ ์ฐจ์ด + +### 1.1 screen_layouts (V1) - ๋ณธ์„œ๋ฒ„ + +```sql +-- ์ปดํฌ๋„ŒํŠธ๋ณ„ 1๊ฐœ ๋ ˆ์ฝ”๋“œ +CREATE TABLE screen_layouts ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER, + component_type VARCHAR(50), + component_id VARCHAR(100), + properties JSONB, -- ๋ชจ๋“  ์„ค์ •๊ฐ’ ํฌํ•จ + ... +); +``` + +**ํŠน์ง•:** +- ํ™”๋ฉด๋‹น N๊ฐœ ๋ ˆ์ฝ”๋“œ (์ปดํฌ๋„ŒํŠธ ์ˆ˜๋งŒํผ) +- `properties`์— ๋ชจ๋“  ์„ค์ • ์ €์žฅ (defaults + overrides ๊ตฌ๋ถ„ ์—†์Œ) +- `menu_objid` ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ + +### 1.2 screen_layouts_v2 - ๊ฐœ๋ฐœ์„œ๋ฒ„ + +```sql +-- ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +CREATE TABLE screen_layouts_v2 ( + layout_id SERIAL PRIMARY KEY, + screen_id INTEGER NOT NULL, + company_code VARCHAR(20) NOT NULL, + layout_data JSONB NOT NULL DEFAULT '{}'::jsonb, + UNIQUE(screen_id, company_code) +); +``` + +**layout_data ๊ตฌ์กฐ:** +```json +{ + "version": "2.0", + "components": [ + { + "id": "comp_xxx", + "url": "@/lib/registry/components/v2-table-list", + "position": { "x": 0, "y": 0 }, + "size": { "width": 100, "height": 50 }, + "displayOrder": 0, + "overrides": { + "tableName": "inspection_standard", + "columns": ["id", "name"] + } + } + ], + "updatedAt": "2026-02-03T12:00:00Z" +} +``` + +**ํŠน์ง•:** +- ํ™”๋ฉด๋‹น 1๊ฐœ ๋ ˆ์ฝ”๋“œ +- `url` + `overrides` ๋ฐฉ์‹ (Zod ์Šคํ‚ค๋งˆ defaults์™€ ๋ณ‘ํ•ฉ) +- `table_name + column_name` ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ/์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ (์ „์—ญ) + +--- + +## 2. ๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ด€๋ฆฌ ๊ตฌ์กฐ (V2) + +### 2.1 ํ•ต์‹ฌ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ + +``` +table_type_columns (์ปฌ๋Ÿผ ํƒ€์ž… ์ •์˜) +โ”œโ”€โ”€ input_type = 'category' โ†’ category_values +โ”œโ”€โ”€ input_type = 'numbering' โ†’ numbering_rules +โ””โ”€โ”€ input_type = 'text', 'date', 'number', etc. +``` + +### 2.2 table_type_columns + +๊ฐ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๋ณ„ ์ž…๋ ฅ ํƒ€์ž…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +```sql +SELECT table_name, column_name, input_type, column_label +FROM table_type_columns +WHERE input_type IN ('category', 'numbering'); +``` + +**์ฃผ์š” input_type:** +| input_type | ์„ค๋ช… | ์—ฐ๊ฒฐ ํ…Œ์ด๋ธ” | +|------------|------|-------------| +| text | ํ…์ŠคํŠธ ์ž…๋ ฅ | - | +| number | ์ˆซ์ž ์ž…๋ ฅ | - | +| date | ๋‚ ์งœ ์ž…๋ ฅ | - | +| category | ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด | category_values | +| numbering | ์ž๋™ ์ฑ„๋ฒˆ | numbering_rules | +| entity | ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ | - | + +### 2.3 category_values (์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ฆฌ) + +```sql +-- ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์กฐํšŒ +SELECT value_id, table_name, column_name, value_code, value_label, + parent_value_id, depth, company_code +FROM category_values +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_method' + AND company_code = 'COMPANY_7'; +``` + +**V1 vs V2 ์ฐจ์ด:** +| ๊ตฌ๋ถ„ | V1 | V2 | +|------|----|----| +| ํ‚ค | menu_objid | table_name + column_name | +| ๋ฒ”์œ„ | ํ™”๋ฉด๋ณ„ | ์ „์—ญ (ํ…Œ์ด๋ธ”.์ปฌ๋Ÿผ๋ณ„) | +| ๊ณ„์ธต | ๋‹จ์ผ | 3๋‹จ๊ณ„ (๋Œ€/์ค‘/์†Œ๋ถ„๋ฅ˜) | + +### 2.4 numbering_rules (์ฑ„๋ฒˆ ๊ทœ์น™) + +```sql +-- ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ +SELECT rule_id, rule_name, table_name, column_name, separator, + reset_period, current_sequence, company_code +FROM numbering_rules +WHERE company_code = 'COMPANY_7'; +``` + +**์—ฐ๊ฒฐ ๋ฐฉ์‹:** +``` +table_type_columns.detail_settings = '{"numberingRuleId": "rule-xxx"}' + โ†“ + numbering_rules.rule_id = "rule-xxx" +``` + +--- + +## 3. ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +### 3.1 ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | ๋น„๊ณ  | +|-------------|---------------|------| +| table-list | v2-table-list | ํ…Œ์ด๋ธ” ๋ชฉ๋ก | +| button-primary | v2-button-primary | ๋ฒ„ํŠผ | +| text-input | v2-text-input | ํ…์ŠคํŠธ ์ž…๋ ฅ | +| select-basic | v2-select | ๋“œ๋กญ๋‹ค์šด | +| date-input | v2-date-input | ๋‚ ์งœ ์ž…๋ ฅ | +| entity-search-input | v2-entity-search | ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ | +| tabs-widget | v2-tabs-widget | ํƒญ | + +### 3.2 ํŠน์ˆ˜ ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ + +| V1 (๋ณธ์„œ๋ฒ„) | V2 (๊ฐœ๋ฐœ์„œ๋ฒ„) | ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฐฉ์‹ | +|-------------|---------------|-------------------| +| category-manager | v2-category-manager | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| numbering-rule | v2-numbering-rule | table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| ๋ชจ๋‹ฌ ํ™”๋ฉด | overlay ํ†ตํ•ฉ | ๋ถ€๋ชจ ํ™”๋ฉด์— ํ†ตํ•ฉ | + +### 3.3 ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ณ€๊ฒฝ + +**V1 (๋ณธ์„œ๋ฒ„):** +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ ๋ฒ„ํŠผ ํด๋ฆญ โ†’ ํ™”๋ฉด B (screen_id: 143) - ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ +``` + +**V2 (๊ฐœ๋ฐœ์„œ๋ฒ„):** +``` +ํ™”๋ฉด A (screen_id: 142) - ๊ฒ€์‚ฌ์žฅ๋น„๊ด€๋ฆฌ + โ””โ”€โ”€ v2-dialog-form ์ปดํฌ๋„ŒํŠธ๋กœ ๋ชจ๋‹ฌ ํ†ตํ•ฉ +``` + +--- + +## 4. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ˆ์ฐจ + +### 4.1 ์‚ฌ์ „ ๋ถ„์„ + +```sql +-- 1. ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name, + COUNT(sl.layout_id) as component_count +FROM screen_definitions sd +LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sd.screen_code LIKE 'COMPANY_7_%' + AND sd.screen_name LIKE '%ํ’ˆ์งˆ%' +GROUP BY sd.screen_id, sd.screen_code, sd.screen_name, sd.table_name; + +-- 2. ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ™”๋ฉด ํ˜„ํ™ฉ ํ™•์ธ +SELECT sd.screen_id, sd.screen_code, sd.screen_name, + sv2.layout_data IS NOT NULL as has_v2_layout +FROM screen_definitions sd +LEFT JOIN screen_layouts_v2 sv2 ON sd.screen_id = sv2.screen_id +WHERE sd.company_code = 'COMPANY_7'; +``` + +### 4.2 Step 1: screen_definitions ๋™๊ธฐํ™” + +```sql +-- ๋ณธ์„œ๋ฒ„์—๋งŒ ์žˆ๋Š” ํ™”๋ฉด์„ ๊ฐœ๋ฐœ์„œ๋ฒ„์— ์ถ”๊ฐ€ +INSERT INTO screen_definitions (screen_code, screen_name, table_name, company_code, ...) +SELECT screen_code, screen_name, table_name, company_code, ... +FROM [๋ณธ์„œ๋ฒ„].screen_definitions +WHERE screen_code NOT IN (SELECT screen_code FROM screen_definitions); +``` + +### 4.3 Step 2: V1 โ†’ V2 ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™˜ + +```typescript +// ๋ณ€ํ™˜ ๋กœ์ง (pseudo-code) +async function convertV1toV2(screenId: number, companyCode: string) { + // 1. V1 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const v1Layouts = await getV1Layouts(screenId); + + // 2. V2 ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const v2Layout = { + version: "2.0", + components: v1Layouts.map(v1 => ({ + id: v1.component_id, + url: mapComponentUrl(v1.component_type), + position: { x: v1.position_x, y: v1.position_y }, + size: { width: v1.width, height: v1.height }, + displayOrder: v1.display_order, + overrides: extractOverrides(v1.properties) + })), + updatedAt: new Date().toISOString() + }; + + // 3. V2 ํ…Œ์ด๋ธ”์— ์ €์žฅ + await saveV2Layout(screenId, companyCode, v2Layout); +} + +function mapComponentUrl(v1Type: string): string { + const mapping = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + // ... ๊ธฐํƒ€ ๋งคํ•‘ + }; + return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`; +} +``` + +### 4.4 Step 3: ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +```sql +-- ๋ณธ์„œ๋ฒ„ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ category_values +INSERT INTO category_values ( + table_name, column_name, value_code, value_label, + value_order, parent_value_id, depth, company_code +) +SELECT + -- V1 ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ table_name + column_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€ํ™˜ + 'inspection_standard' as table_name, + 'inspection_method' as column_name, + value_code, + value_label, + sort_order, + NULL as parent_value_id, + 1 as depth, + 'COMPANY_7' as company_code +FROM [๋ณธ์„œ๋ฒ„_์นดํ…Œ๊ณ ๋ฆฌ_๋ฐ์ดํ„ฐ]; +``` + +### 4.5 Step 4: ์ฑ„๋ฒˆ ๊ทœ์น™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +```sql +-- ๋ณธ์„œ๋ฒ„ ์ฑ„๋ฒˆ ๊ทœ์น™ โ†’ ๊ฐœ๋ฐœ์„œ๋ฒ„ numbering_rules +INSERT INTO numbering_rules ( + rule_id, rule_name, table_name, column_name, + separator, reset_period, current_sequence, company_code +) +SELECT + rule_id, + rule_name, + 'inspection_standard' as table_name, + 'inspection_code' as column_name, + separator, + reset_period, + 0 as current_sequence, -- ์‹œํ€€์Šค ์ดˆ๊ธฐํ™” + 'COMPANY_7' as company_code +FROM [๋ณธ์„œ๋ฒ„_์ฑ„๋ฒˆ_๊ทœ์น™]; +``` + +### 4.6 Step 5: table_type_columns ์„ค์ • + +```sql +-- ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์„ค์ • +UPDATE table_type_columns +SET input_type = 'category' +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_method' + AND company_code = 'COMPANY_7'; + +-- ์ฑ„๋ฒˆ ์ปฌ๋Ÿผ ์„ค์ • +UPDATE table_type_columns +SET + input_type = 'numbering', + detail_settings = '{"numberingRuleId": "rule-xxx"}' +WHERE table_name = 'inspection_standard' + AND column_name = 'inspection_code' + AND company_code = 'COMPANY_7'; +``` + +--- + +## 5. ํ’ˆ์งˆ๊ด€๋ฆฌ ๋ฉ”๋‰ด ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ˜„ํ™ฉ + +### 5.1 ํ™”๋ฉด ๋งคํ•‘ ํ˜„ํ™ฉ + +| ๋ณธ์„œ๋ฒ„ ์ฝ”๋“œ | ํ™”๋ฉด๋ช… | ํ…Œ์ด๋ธ” | ๊ฐœ๋ฐœ์„œ๋ฒ„ ์ƒํƒœ | ๋น„๊ณ  | +|-------------|--------|--------|---------------|------| +| COMPANY_7_126 | ๊ฒ€์‚ฌ์ •๋ณด ๊ด€๋ฆฌ | inspection_standard | โœ… V2 ์กด์žฌ | ์ปดํฌ๋„ŒํŠธ ์ˆ˜ ํ™•์ธ ํ•„์š” | +| COMPANY_7_127 | ํ’ˆ๋ชฉ์˜ต์…˜ ์„ค์ • | - | โœ… V2 ์กด์žฌ | v2-category-manager ์‚ฌ์šฉ์ค‘ | +| COMPANY_7_138 | ์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | V2: table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_139 | ์ฝ”๋“œ ์„ค์ • | inspection_standard | โŒ ๋ˆ„๋ฝ | V2: table_name ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ | +| COMPANY_7_142 | ๊ฒ€์‚ฌ์žฅ๋น„ ๊ด€๋ฆฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_143 | ๊ฒ€์‚ฌ์žฅ๋น„ ๋“ฑ๋ก๋ชจ๋‹ฌ | inspection_equipment_mng | โŒ ๋ˆ„๋ฝ | COMPANY_7_142์— ํ†ตํ•ฉ | +| COMPANY_7_144 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ์ •๋ณด | defect_standard_mng | โŒ ๋ˆ„๋ฝ | ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ•„์š” | +| COMPANY_7_145 | ๋ถˆ๋Ÿ‰๊ธฐ์ค€ ๋“ฑ๋ก๋ชจ๋‹ฌ | defect_standard_mng | โŒ ๋ˆ„๋ฝ | COMPANY_7_144์— ํ†ตํ•ฉ | + +### 5.2 ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ์ปฌ๋Ÿผ ํ˜„ํ™ฉ + +**inspection_standard:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| inspection_method | category | ๊ฒ€์‚ฌ๋ฐฉ๋ฒ• | +| unit | category | ๋‹จ์œ„ | +| apply_type | category | ์ ์šฉ๊ตฌ๋ถ„ | +| inspection_type | category | ์œ ํ˜• | + +**inspection_equipment_mng:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| equipment_type | category | ์žฅ๋น„์œ ํ˜• | +| installation_location | category | ์„ค์น˜์žฅ์†Œ | +| equipment_status | category | ์žฅ๋น„์ƒํƒœ | + +**defect_standard_mng:** +| ์ปฌ๋Ÿผ | input_type | ๋ผ๋ฒจ | +|------|------------|------| +| defect_type | category | ๋ถˆ๋Ÿ‰์œ ํ˜• | +| severity | category | ์‹ฌ๊ฐ๋„ | +| inspection_type | category | ๊ฒ€์‚ฌ์œ ํ˜• | + +--- + +## 6. ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ + +### 6.1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ + +```typescript +// backend-node/src/scripts/migrateV1toV2.ts +import { getPool } from "../database/db"; + +interface MigrationResult { + screenCode: string; + success: boolean; + message: string; + componentCount?: number; +} + +async function migrateScreenToV2( + screenCode: string, + companyCode: string +): Promise { + const pool = getPool(); + + try { + // 1. V1 ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ (๋ณธ์„œ๋ฒ„์—์„œ) + const v1Result = await pool.query(` + SELECT sl.*, sd.table_name, sd.screen_name + FROM screen_layouts sl + JOIN screen_definitions sd ON sl.screen_id = sd.screen_id + WHERE sd.screen_code = $1 + ORDER BY sl.display_order + `, [screenCode]); + + if (v1Result.rows.length === 0) { + return { screenCode, success: false, message: "V1 ๋ ˆ์ด์•„์›ƒ ์—†์Œ" }; + } + + // 2. V2 ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const components = v1Result.rows + .filter(row => row.component_type !== '_metadata') + .map(row => ({ + id: row.component_id || `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + url: mapComponentUrl(row.component_type), + position: { x: row.position_x || 0, y: row.position_y || 0 }, + size: { width: row.width || 100, height: row.height || 50 }, + displayOrder: row.display_order || 0, + overrides: extractOverrides(row.properties, row.component_type) + })); + + const layoutData = { + version: "2.0", + components, + migratedFrom: "V1", + migratedAt: new Date().toISOString() + }; + + // 3. ๊ฐœ๋ฐœ์„œ๋ฒ„ V2 ํ…Œ์ด๋ธ”์— ์ €์žฅ + const screenId = v1Result.rows[0].screen_id; + + await pool.query(` + INSERT INTO screen_layouts_v2 (screen_id, company_code, layout_data) + VALUES ($1, $2, $3) + ON CONFLICT (screen_id, company_code) + DO UPDATE SET layout_data = $3, updated_at = NOW() + `, [screenId, companyCode, JSON.stringify(layoutData)]); + + return { + screenCode, + success: true, + message: "๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ", + componentCount: components.length + }; + } catch (error: any) { + return { screenCode, success: false, message: error.message }; + } +} + +function mapComponentUrl(v1Type: string): string { + const mapping: Record = { + 'table-list': '@/lib/registry/components/v2-table-list', + 'button-primary': '@/lib/registry/components/v2-button-primary', + 'text-input': '@/lib/registry/components/v2-text-input', + 'select-basic': '@/lib/registry/components/v2-select', + 'date-input': '@/lib/registry/components/v2-date-input', + 'entity-search-input': '@/lib/registry/components/v2-entity-search', + 'category-manager': '@/lib/registry/components/v2-category-manager', + 'numbering-rule': '@/lib/registry/components/v2-numbering-rule', + 'tabs-widget': '@/lib/registry/components/v2-tabs-widget', + 'textarea-basic': '@/lib/registry/components/v2-textarea', + }; + return mapping[v1Type] || `@/lib/registry/components/v2-${v1Type}`; +} + +function extractOverrides(properties: any, componentType: string): Record { + if (!properties) return {}; + + // V2 Zod ์Šคํ‚ค๋งˆ defaults์™€ ๋น„๊ตํ•˜์—ฌ ๋‹ค๋ฅธ ๊ฐ’๋งŒ ์ถ”์ถœ + // (์‹ค์ œ ๊ตฌํ˜„ ์‹œ ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ defaultConfig์™€ ๋น„๊ต) + const overrides: Record = {}; + + // ํ•„์ˆ˜ ์„ค์ •๋งŒ ์ถ”์ถœ + if (properties.tableName) overrides.tableName = properties.tableName; + if (properties.columns) overrides.columns = properties.columns; + if (properties.label) overrides.label = properties.label; + if (properties.onClick) overrides.onClick = properties.onClick; + + return overrides; +} +``` + +--- + +## 7. ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 7.1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ + +- [ ] ๋ณธ์„œ๋ฒ„ ํ™”๋ฉด ๋ชฉ๋ก ํ™•์ธ +- [ ] ๊ฐœ๋ฐœ์„œ๋ฒ„ ๊ธฐ์กด V2 ๋ฐ์ดํ„ฐ ๋ฐฑ์—… +- [ ] ์ปดํฌ๋„ŒํŠธ ๋งคํ•‘ ํ…Œ์ด๋ธ” ๊ฒ€ํ†  +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ/์ฑ„๋ฒˆ ๋ฐ์ดํ„ฐ ๋ถ„์„ + +### 7.2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ›„ + +- [ ] screen_definitions ๋™๊ธฐํ™” ํ™•์ธ +- [ ] screen_layouts_v2 ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ™•์ธ +- [ ] ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ +- [ ] ์นดํ…Œ๊ณ ๋ฆฌ ๋“œ๋กญ๋‹ค์šด ๋™์ž‘ ํ™•์ธ +- [ ] ์ฑ„๋ฒˆ ๊ทœ์น™ ๋™์ž‘ ํ™•์ธ +- [ ] ์ €์žฅ/์ˆ˜์ •/์‚ญ์ œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ + +### 7.3 ๋ชจ๋‹ฌ ํ†ตํ•ฉ ํ™•์ธ + +- [ ] ๊ธฐ์กด ๋ชจ๋‹ฌ ํ™”๋ฉด โ†’ overlay ํ†ตํ•ฉ ์™„๋ฃŒ +- [ ] ๋ถ€๋ชจ-์ž์‹ ๋ฐ์ดํ„ฐ ์—ฐ๋™ ํ™•์ธ +- [ ] ๋ชจ๋‹ฌ ์—ด๊ธฐ/๋‹ซ๊ธฐ ๋™์ž‘ ํ™•์ธ + +--- + +## 8. ๋กค๋ฐฑ ๊ณ„ํš + +๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ์ ˆ์ฐจ: + +```sql +-- 1. V2 ๋ ˆ์ด์•„์›ƒ ๋กค๋ฐฑ +DELETE FROM screen_layouts_v2 +WHERE screen_id IN ( + SELECT screen_id FROM screen_definitions + WHERE screen_code LIKE 'COMPANY_7_%' +); + +-- 2. ์ถ”๊ฐ€๋œ screen_definitions ๋กค๋ฐฑ +DELETE FROM screen_definitions +WHERE screen_code IN ('์‹ ๊ทœ_์ถ”๊ฐ€๋œ_์ฝ”๋“œ๋“ค') + AND company_code = 'COMPANY_7'; + +-- 3. category_values ๋กค๋ฐฑ +DELETE FROM category_values +WHERE company_code = 'COMPANY_7' + AND created_at > '[๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์‹œ์ž‘_์‹œ๊ฐ„]'; + +-- 4. numbering_rules ๋กค๋ฐฑ +DELETE FROM numbering_rules +WHERE company_code = 'COMPANY_7' + AND created_at > '[๋งˆ์ด๊ทธ๋ ˆ์ด์…˜_์‹œ์ž‘_์‹œ๊ฐ„]'; +``` + +--- + +## 9. ์ฐธ๊ณ  ์ž๋ฃŒ + +### ๊ด€๋ จ ์ฝ”๋“œ ํŒŒ์ผ + +- **V2 Category Manager**: `frontend/lib/registry/components/v2-category-manager/` +- **V2 Numbering Rule**: `frontend/lib/registry/components/v2-numbering-rule/` +- **Category Service**: `backend-node/src/services/categoryTreeService.ts` +- **Numbering Service**: `backend-node/src/services/numberingRuleService.ts` + +### ๊ด€๋ จ ๋ฌธ์„œ + +- [V2 ์ปดํฌ๋„ŒํŠธ ๋ถ„์„ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_๋ถ„์„_๊ฐ€์ด๋“œ.md) +- [V2 ์ปดํฌ๋„ŒํŠธ ์—ฐ๋™ ๊ฐ€์ด๋“œ](../V2_์ปดํฌ๋„ŒํŠธ_์—ฐ๋™_๊ฐ€์ด๋“œ.md) +- [ํ™”๋ฉด ๊ฐœ๋ฐœ ํ‘œ์ค€ ๊ฐ€์ด๋“œ](../screen-implementation-guide/SCREEN_DEVELOPMENT_STANDARD.md) +- [์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์•„์›ƒ V2 ์•„ํ‚คํ…์ฒ˜](./COMPONENT_LAYOUT_V2_ARCHITECTURE.md) + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ์ž‘์„ฑ์ž | ๋‚ด์šฉ | +|------|--------|------| +| 2026-02-03 | DDD1542 | ์ดˆ์•ˆ ์ž‘์„ฑ | diff --git a/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md b/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md index 27946afa..aea92243 100644 --- a/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md +++ b/docs/DDD1542/ํ™”๋ฉด๊ด€๊ณ„_์‹œ๊ฐํ™”_๊ฐœ์„ _๋ณด๊ณ ์„œ.md @@ -23,7 +23,8 @@ | ํ…Œ์ด๋ธ”๋ช… | ์šฉ๋„ | ์ฃผ์š” ์ปฌ๋Ÿผ | |----------|------|----------| | `screen_definitions` | ํ™”๋ฉด ์ •์˜ ์ •๋ณด | `screen_id`, `screen_name`, `table_name`, `company_code` | -| `screen_layouts` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด | `screen_id`, `properties` (JSONB - componentConfig ํฌํ•จ) | +| `screen_layouts` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด (Legacy) | `screen_id`, `properties` (JSONB - componentConfig ํฌํ•จ) | +| `screen_layouts_v2` | ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ ์ •๋ณด (V2) | `screen_id`, `layout_data` (JSONB - components ๋ฐฐ์—ด) | | `screen_groups` | ํ™”๋ฉด ๊ทธ๋ฃน ์ •๋ณด | `group_id`, `group_code`, `group_name`, `parent_group_id` | | `screen_group_mappings` | ํ™”๋ฉด-๊ทธ๋ฃน ๋งคํ•‘ | `group_id`, `screen_id`, `display_order` | @@ -86,9 +87,17 @@ screen_groups (๊ทธ๋ฃน) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€ screen_definitions (ํ™”๋ฉด) โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€ screen_layouts (๋ ˆ์ด์•„์›ƒ/์ปดํฌ๋„ŒํŠธ) + โ”‚ โ”œโ”€โ”€โ”€ screen_layouts (Legacy) + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€โ”€โ”€ properties.componentConfig + โ”‚ โ”‚ โ”œโ”€โ”€ fieldMappings + โ”‚ โ”‚ โ”œโ”€โ”€ parentDataMapping + โ”‚ โ”‚ โ”œโ”€โ”€ columns.mapping + โ”‚ โ”‚ โ””โ”€โ”€ rightPanel.relation + โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€โ”€ screen_layouts_v2 (V2) โ† ํ˜„์žฌ ํ‘œ์ค€ โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€ properties.componentConfig + โ”‚ โ””โ”€โ”€โ”€ layout_data.components[].overrides โ”‚ โ”œโ”€โ”€ fieldMappings โ”‚ โ”œโ”€โ”€ parentDataMapping โ”‚ โ”œโ”€โ”€ columns.mapping @@ -1120,9 +1129,12 @@ screenSubTables[screenId].subTables.push({ 21. [x] ํ•„ํ„ฐ ์—ฐ๊ฒฐ์„  ํฌ์ปค์‹ฑ ์ œ์–ด (ํ•ด๋‹น ํ™”๋ฉด ํฌ์ปค์‹ฑ ์‹œ์—๋งŒ ํ‘œ์‹œ) 22. [x] ์ €์žฅ ํ…Œ์ด๋ธ” ์ œ์™ธ ์กฐ๊ฑด ์ถ”๊ฐ€ (table-list + ์ฒดํฌ๋ฐ•์Šค + openModalWithData) 23. [x] ์ฒซ ์ง„์ž… ์‹œ ํฌ์ปค์‹ฑ ์—†์ด ์‹œ์ž‘ (ํŠธ๋ฆฌ์—์„œ ํ™”๋ฉด ํด๋ฆญ ์‹œ ๊ทธ๋ฃน๋งŒ ์ง„์ž…) -24. [ ] **์„  ๊ต์ฐจ์  ์ด์งˆ๊ฐ ํ•ด๊ฒฐ** (๊ณ„ํš ์ค‘) -22. [ ] ๋ฒ”๋ก€ UI ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) -23. [ ] ์—ฃ์ง€ ๋ผ๋ฒจ์— ๊ด€๊ณ„ ์œ ํ˜• ํ‘œ์‹œ (์„ ํƒ์‚ฌํ•ญ) +24. [x] **screen_layouts_v2 ์ง€์› ์ถ”๊ฐ€** (rightPanel.relation V2 UNION ์ฟผ๋ฆฌ) โœ… 2026-01-30 +25. [x] **ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์šฐ์„ ์ˆœ์œ„ ์‹œ์Šคํ…œ** (๋ฉ”์ธ > ์„œ๋ธŒ ์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) โœ… 2026-01-30 +26. [x] **globalMainTables API ์ถ”๊ฐ€** (WHERE ์กฐ๊ฑด ๋Œ€์ƒ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋ฐ˜ํ™˜) โœ… 2026-01-30 +27. [ ] **์„  ๊ต์ฐจ์  ์ด์งˆ๊ฐ ํ•ด๊ฒฐ** (๊ณ„ํš ์ค‘) +28. [ ] ๋ฒ”๋ก€ UI ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) +29. [ ] ์—ฃ์ง€ ๋ผ๋ฒจ์— ๊ด€๊ณ„ ์œ ํ˜• ํ‘œ์‹œ (์„ ํƒ์‚ฌํ•ญ) --- @@ -1682,6 +1694,149 @@ frontend/ --- +## ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์šฐ์„ ์ˆœ์œ„ ์‹œ์Šคํ…œ (2026-01-30) + +### ๋ฐฐ๊ฒฝ + +๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ ๊ด€๊ณ„์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”(์˜ˆ: `user_dept`)์ด ๋‹ค๋ฅธ ๊ณณ์—์„œ autocomplete ์ฐธ์กฐ๋กœ๋„ ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ, +์„œ๋ธŒ ํ…Œ์ด๋ธ” ์˜์—ญ์— ์ž˜๋ชป ๋ฐฐ์น˜๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. + +### ๋ฌธ์ œ ์ƒํ™ฉ + +``` +[user_info] - ํ™”๋ฉด 139์˜ ๋””ํ…Œ์ผ โ†’ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์˜์—ญ (O) +[user_dept] - ํ™”๋ฉด 162์˜ ๋””ํ…Œ์ผ์ด์ง€๋งŒ autocomplete ์ฐธ์กฐ๋„ ์žˆ์Œ โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์˜์—ญ (X) +``` + +**์›์ธ**: ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ ์‹œ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์—†์–ด์„œ ๋จผ์ € ๋ฐœ๊ฒฌ๋œ ๊ด€๊ณ„ ํƒ€์ž…์œผ๋กœ ๋ถ„๋ฅ˜๋จ + +### ํ•ด๊ฒฐ์ฑ…: ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ + +#### ๋ถ„๋ฅ˜ ๊ทœ์น™ + +| ์šฐ์„ ์ˆœ์œ„ | ๋ถ„๋ฅ˜ | ์กฐ๊ฑด | ๋น„๊ณ  | +|----------|------|------|------| +| **1์ˆœ์œ„** | ๋ฉ”์ธ ํ…Œ์ด๋ธ” | `screen_definitions.table_name` | ์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ | +| **1์ˆœ์œ„** | ๋ฉ”์ธ ํ…Œ์ด๋ธ” | `v2-split-panel-layout.rightPanel.tableName` | WHERE ์กฐ๊ฑด ๋Œ€์ƒ | +| **2์ˆœ์œ„** | ์„œ๋ธŒ ํ…Œ์ด๋ธ” | ์กฐ์ธ์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” | autocomplete ๋“ฑ ์ฐธ์กฐ | + +#### ํ•ต์‹ฌ ๊ทœ์น™ + +> **๋ฉ”์ธ ์กฐ๊ฑด์— ํ•ด๋‹นํ•˜๋ฉด, ์„œ๋ธŒ ์กฐ๊ฑด์ด ์žˆ์–ด๋„ ๋ฌด์กฐ๊ฑด ๋ฉ”์ธ์œผ๋กœ ๋ถ„๋ฅ˜** + +### ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ (`screenGroupController.ts`) + +#### 1. screen_layouts_v2 ์ง€์› ์ถ”๊ฐ€ + +`rightPanelQuery`์— V2 ํ…Œ์ด๋ธ” UNION ์ถ”๊ฐ€: + +```sql +-- V1: screen_layouts์—์„œ ์กฐํšŒ +SELECT ... +FROM screen_definitions sd +JOIN screen_layouts sl ON sd.screen_id = sl.screen_id +WHERE sl.properties->'componentConfig'->'rightPanel'->'relation' IS NOT NULL + +UNION ALL + +-- V2: screen_layouts_v2์—์„œ ์กฐํšŒ (v2-split-panel-layout ์ปดํฌ๋„ŒํŠธ) +SELECT + sd.screen_id, + comp->'overrides'->>'type' as component_type, + comp->'overrides'->'rightPanel'->'relation' as right_panel_relation, + comp->'overrides'->'rightPanel'->>'tableName' as right_panel_table, + ... +FROM screen_definitions sd +JOIN screen_layouts_v2 slv2 ON sd.screen_id = slv2.screen_id, +jsonb_array_elements(slv2.layout_data->'components') as comp +WHERE comp->'overrides'->'rightPanel'->'relation' IS NOT NULL +``` + +#### 2. globalMainTables API ์ถ”๊ฐ€ + +`getScreenSubTables` ์‘๋‹ต์— ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ถ”๊ฐ€: + +```sql +-- ๋ชจ๋“  ํ™”๋ฉด์˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ +SELECT DISTINCT table_name as main_table FROM screen_definitions WHERE screen_id = ANY($1) +UNION +SELECT DISTINCT comp->'overrides'->'rightPanel'->>'tableName' as main_table +FROM screen_layouts_v2 ... +``` + +**์‘๋‹ต ๊ตฌ์กฐ:** +```typescript +res.json({ + success: true, + data: screenSubTables, + globalMainTables: globalMainTables, // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ถ”๊ฐ€ +}); +``` + +### ํ”„๋ก ํŠธ์—”๋“œ ๋ณ€๊ฒฝ (`ScreenRelationFlow.tsx`) + +#### 1. globalMainTables ์ƒํƒœ ์ถ”๊ฐ€ + +```typescript +const [globalMainTables, setGlobalMainTables] = useState>(new Set()); +``` + +#### 2. ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ + +```typescript +// 1. globalMainTables๋ฅผ mainTableSet์— ๋จผ์ € ์ถ”๊ฐ€ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) +globalMainTables.forEach((tableName) => { + if (!mainTableSet.has(tableName)) { + mainTableSet.add(tableName); + filterTableSet.add(tableName); // ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ + } +}); + +// 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (mainTableSet์— ์—†๋Š” ๊ฒƒ๋งŒ) +screenSubData.subTables.forEach((subTable) => { + if (mainTableSet.has(subTable.tableName)) { + return; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ์—์„œ ์ œ์™ธ + } + subTableSet.add(subTable.tableName); +}); +``` + +### ์‹œ๊ฐ์  ๊ฒฐ๊ณผ + +#### ๋ณ€๊ฒฝ ์ „ + +``` +[ํ™”๋ฉด ๋…ธ๋“œ๋“ค] + โ”‚ + โ–ผ +[๋ฉ”์ธ ํ…Œ์ด๋ธ”: dept_info, user_info] โ† user_dept ์—†์Œ + โ”‚ + โ–ผ +[์„œ๋ธŒ ํ…Œ์ด๋ธ”: user_dept, customer_mng] โ† user_dept๊ฐ€ ์ž˜๋ชป ๋ฐฐ์น˜๋จ +``` + +#### ๋ณ€๊ฒฝ ํ›„ + +``` +[ํ™”๋ฉด ๋…ธ๋“œ๋“ค] + โ”‚ + โ–ผ +[๋ฉ”์ธ ํ…Œ์ด๋ธ”: dept_info, user_info, user_dept] โ† user_dept ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ + โ”‚ + โ–ผ +[์„œ๋ธŒ ํ…Œ์ด๋ธ”: customer_mng] โ† ์กฐ์ธ ์ฐธ์กฐ์šฉ ํ…Œ์ด๋ธ”๋งŒ +``` + +### ๊ด€๋ จ ํŒŒ์ผ + +| ํŒŒ์ผ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|----------| +| `backend-node/src/controllers/screenGroupController.ts` | screen_layouts_v2 UNION ์ถ”๊ฐ€, globalMainTables ๋ฐ˜ํ™˜ | +| `frontend/components/screen/ScreenRelationFlow.tsx` | globalMainTables ์ƒํƒœ, ์šฐ์„ ์ˆœ์œ„ ๋ถ„๋ฅ˜ ๋กœ์ง | +| `frontend/components/screen/ScreenNode.tsx` | isFilterTable prop ๋ฐ ๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ ์Šคํƒ€์ผ | + +--- + ## ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ ๊ฐœ์„  (2026-01-12) ### ๊ฐœ์š” @@ -1742,4 +1897,6 @@ npm install react-zoom-pan-pinch - [๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ](.cursor/rules/multi-tenancy-guide.mdc) - [API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ ๊ทœ์น™](.cursor/rules/api-client-usage.mdc) - [๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ](.cursor/rules/admin-page-style-guide.mdc) +- [ํ™”๋ฉด ๋ณต์ œ V2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš์„œ](../SCREEN_COPY_V2_MIGRATION_PLAN.md) - screen_layouts_v2 ๋ณต์ œ ๋กœ์ง +- [V2 ์ปดํฌ๋„ŒํŠธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ„์„](../V2_COMPONENT_MIGRATION_ANALYSIS.md) - V2 ์•„ํ‚คํ…์ฒ˜ diff --git a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md index 7e1afcba..c60f1dfb 100644 --- a/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md +++ b/docs/SCREEN_COPY_V2_MIGRATION_PLAN.md @@ -467,9 +467,9 @@ V2 ์ „ํ™˜ ๋กค๋ฐฑ (ํ•„์š”์‹œ): - [x] copyScreens() - Legacy ์ œ๊ฑฐ, V2๋กœ ๊ต์ฒด โœ… 2026-01-28 - [x] hasLayoutChangesV2() ํ•จ์ˆ˜ ์ถ”๊ฐ€ โœ… 2026-01-28 - [x] updateTabScreenReferences() V2 ์ง€์› ์ถ”๊ฐ€ โœ… 2026-01-28 -- [ ] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ -- [ ] V2 ์ „์šฉ ๋ณต์ œ ๋™์ž‘ ํ™•์ธ +- [x] ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โœ… 2026-01-30 +- [x] V2 ์ „์šฉ ๋ณต์ œ ๋™์ž‘ ํ™•์ธ โœ… 2026-01-30 ### 9.3 Phase 2 ์™„๋ฃŒ ์กฐ๊ฑด @@ -522,3 +522,4 @@ V2 ์ „ํ™˜ ๋กค๋ฐฑ (ํ•„์š”์‹œ): | 2026-01-28 | ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฒ€์ฆ - updateTabScreenReferences V2 ์ง€์› ์ถ”๊ฐ€ | Claude | | 2026-01-28 | V2 ๊ฒฝ๋กœ ์ง€์› ์ถ”๊ฐ€ - action/sections ์ง์ ‘ ๊ฒฝ๋กœ (componentConfig ์—†์ด) | Claude | | 2026-01-30 | **์‹ค์ œ ์ฝ”๋“œ ๊ตฌํ˜„ ์™„๋ฃŒ** - copyScreen(), copyScreens() V2 ์ „ํ™˜ | Claude | +| 2026-01-30 | **Phase 1 ํ…Œ์ŠคํŠธ ์™„๋ฃŒ** - ๋‹จ์œ„/ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ํ™•์ธ | Claude | \ No newline at end of file diff --git a/docs/v2-sales-order-modal-layout.json b/docs/v2-sales-order-modal-layout.json new file mode 100644 index 00000000..6c8287e0 --- /dev/null +++ b/docs/v2-sales-order-modal-layout.json @@ -0,0 +1,557 @@ +{ + "version": "2.0", + "screenResolution": { + "width": 1400, + "height": 900, + "name": "์ˆ˜์ฃผ๋“ฑ๋ก ๋ชจ๋‹ฌ", + "category": "modal" + }, + "components": [ + { + "id": "section-options", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 20, "z": 1 }, + "size": { "width": 1360, "height": 80 }, + "overrides": { + "componentConfig": { + "title": "", + "showHeader": false, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 0 + }, + { + "id": "select-input-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "์ž…๋ ฅ ๋ฐฉ์‹", + "columnName": "input_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "customer_first", "label": "๊ฑฐ๋ž˜์ฒ˜ ์šฐ์„ " }, + { "value": "item_first", "label": "ํ’ˆ๋ชฉ ์šฐ์„ " } + ], + "placeholder": "์ž…๋ ฅ ๋ฐฉ์‹ ์„ ํƒ" + }, + "displayOrder": 1 + }, + { + "id": "select-sales-type", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 360, "y": 35, "z": 2 }, + "size": { "width": 300, "height": 40 }, + "overrides": { + "label": "ํŒ๋งค ์œ ํ˜•", + "columnName": "sales_type", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "domestic", "label": "๊ตญ๋‚ด ํŒ๋งค" }, + { "value": "overseas", "label": "ํ•ด์™ธ ํŒ๋งค" } + ], + "placeholder": "ํŒ๋งค ์œ ํ˜• ์„ ํƒ" + }, + "displayOrder": 2 + }, + { + "id": "select-price-method", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 680, "y": 35, "z": 2 }, + "size": { "width": 250, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ๋ฐฉ์‹", + "columnName": "price_method", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "standard", "label": "๊ธฐ์ค€ ๋‹จ๊ฐ€" }, + { "value": "contract", "label": "๊ณ„์•ฝ ๋‹จ๊ฐ€" }, + { "value": "custom", "label": "๊ฐœ๋ณ„ ์ž…๋ ฅ" } + ], + "placeholder": "๋‹จ๊ฐ€ ๋ฐฉ์‹" + }, + "displayOrder": 3 + }, + { + "id": "checkbox-price-edit", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 950, "y": 35, "z": 2 }, + "size": { "width": 150, "height": 40 }, + "overrides": { + "label": "๋‹จ๊ฐ€ ์ˆ˜์ • ํ—ˆ์šฉ", + "columnName": "allow_price_edit", + "mode": "check", + "source": "static", + "options": [{ "value": "Y", "label": "ํ—ˆ์šฉ" }] + }, + "displayOrder": 4 + }, + + { + "id": "section-customer-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 120 }, + "overrides": { + "componentConfig": { + "title": "๊ฑฐ๋ž˜์ฒ˜ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 5 + }, + { + "id": "select-customer", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 155, "z": 3 }, + "size": { "width": 320, "height": 40 }, + "overrides": { + "label": "๊ฑฐ๋ž˜์ฒ˜ *", + "columnName": "partner_id", + "mode": "dropdown", + "source": "entity", + "entityTable": "customer_mng", + "entityValueColumn": "customer_code", + "entityLabelColumn": "customer_name", + "searchable": true, + "placeholder": "๊ฑฐ๋ž˜์ฒ˜๋ช… ์ž…๋ ฅํ•˜์—ฌ ๊ฒ€์ƒ‰", + "required": true, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 6 + }, + { + "id": "input-manager", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 380, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‹ด๋‹น์ž", + "columnName": "manager_name", + "placeholder": "๋‹ด๋‹น์ž", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 7 + }, + { + "id": "input-delivery-partner", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 640, "y": 155, "z": 3 }, + "size": { "width": 240, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์ฒ˜", + "columnName": "delivery_partner_id", + "placeholder": "๋‚ฉํ’ˆ์ฒ˜", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 8 + }, + { + "id": "input-delivery-address", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 900, "y": 155, "z": 3 }, + "size": { "width": 460, "height": 40 }, + "overrides": { + "label": "๋‚ฉํ’ˆ์žฅ์†Œ", + "columnName": "delivery_address", + "placeholder": "๋‚ฉํ’ˆ์žฅ์†Œ", + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 9 + }, + + { + "id": "section-item-first", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 110, "z": 1 }, + "size": { "width": 1360, "height": 200 }, + "overrides": { + "componentConfig": { + "title": "ํ’ˆ๋ชฉ ๋ฐ ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ์ˆ˜์ฃผ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "item_first", + "action": "show" + } + }, + "displayOrder": 10 + }, + + { + "id": "section-items", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 240, "z": 1 }, + "size": { "width": 1360, "height": 280 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€๋œ ํ’ˆ๋ชฉ", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 11 + }, + { + "id": "btn-item-search", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1140, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "ํ’ˆ๋ชฉ ๊ฒ€์ƒ‰", + "action": { + "type": "openModal", + "modalType": "itemSelection" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 12 + }, + { + "id": "btn-shipping-plan", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1250, "y": 245, "z": 5 }, + "size": { "width": 100, "height": 36 }, + "overrides": { + "label": "์ถœํ•˜๊ณ„ํš", + "webTypeConfig": { + "variant": "destructive" + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 13 + }, + { + "id": "repeater-items", + "url": "@/lib/registry/components/v2-repeater", + "position": { "x": 40, "y": 290, "z": 3 }, + "size": { "width": 1320, "height": 200 }, + "overrides": { + "renderMode": "modal", + "dataSource": { + "tableName": "sales_order_detail", + "foreignKey": "order_no", + "referenceKey": "order_no" + }, + "columns": [ + { "field": "part_code", "header": "ํ’ˆ๋ฒˆ", "width": 100 }, + { "field": "part_name", "header": "ํ’ˆ๋ช…", "width": 150 }, + { "field": "spec", "header": "๊ทœ๊ฒฉ", "width": 100 }, + { "field": "unit", "header": "๋‹จ์œ„", "width": 80 }, + { "field": "qty", "header": "์ˆ˜๋Ÿ‰", "width": 100, "editable": true }, + { "field": "unit_price", "header": "๋‹จ๊ฐ€", "width": 100, "editable": true }, + { "field": "amount", "header": "๊ธˆ์•ก", "width": 100 }, + { "field": "due_date", "header": "๋‚ฉ๊ธฐ์ผ", "width": 120, "editable": true } + ], + "modal": { + "sourceTable": "item_info", + "sourceColumns": ["part_code", "part_name", "spec", "material", "unit_price"], + "filterCondition": {} + }, + "features": { + "showAddButton": false, + "showDeleteButton": true, + "inlineEdit": true + }, + "conditionalConfig": { + "enabled": true, + "field": "input_method", + "operator": "=", + "value": "customer_first", + "action": "show" + } + }, + "displayOrder": 14 + }, + + { + "id": "section-trade-info", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 530, "z": 1 }, + "size": { "width": 1360, "height": 150 }, + "overrides": { + "componentConfig": { + "title": "๋ฌด์—ญ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + }, + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 15 + }, + { + "id": "select-incoterms", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 40, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์ธ์ฝ”ํ…€์ฆˆ", + "columnName": "incoterms", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "FOB", "label": "FOB" }, + { "value": "CIF", "label": "CIF" }, + { "value": "EXW", "label": "EXW" }, + { "value": "DDP", "label": "DDP" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 16 + }, + { + "id": "select-payment-term", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 260, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๊ฒฐ์ œ ์กฐ๊ฑด", + "columnName": "payment_term", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "TT", "label": "T/T" }, + { "value": "LC", "label": "L/C" }, + { "value": "DA", "label": "D/A" }, + { "value": "DP", "label": "D/P" } + ], + "placeholder": "์„ ํƒ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 17 + }, + { + "id": "select-currency", + "url": "@/lib/registry/components/v2-select", + "position": { "x": 480, "y": 575, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "ํ†ตํ™”", + "columnName": "currency", + "mode": "dropdown", + "source": "static", + "options": [ + { "value": "KRW", "label": "KRW (์›)" }, + { "value": "USD", "label": "USD (๋‹ฌ๋Ÿฌ)" }, + { "value": "EUR", "label": "EUR (์œ ๋กœ)" }, + { "value": "JPY", "label": "JPY (์—”)" }, + { "value": "CNY", "label": "CNY (์œ„์•ˆ)" } + ], + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 18 + }, + { + "id": "input-port-loading", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "์„ ์ ํ•ญ", + "columnName": "port_of_loading", + "placeholder": "์„ ์ ํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 19 + }, + { + "id": "input-port-discharge", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 260, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "๋„์ฐฉํ•ญ", + "columnName": "port_of_discharge", + "placeholder": "๋„์ฐฉํ•ญ", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 20 + }, + { + "id": "input-hs-code", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 480, "y": 625, "z": 3 }, + "size": { "width": 200, "height": 40 }, + "overrides": { + "label": "HS Code", + "columnName": "hs_code", + "placeholder": "HS Code", + "conditionalConfig": { + "enabled": true, + "field": "sales_type", + "operator": "=", + "value": "overseas", + "action": "show" + } + }, + "displayOrder": 21 + }, + + { + "id": "section-additional", + "url": "@/lib/registry/components/v2-section-card", + "position": { "x": 20, "y": 690, "z": 1 }, + "size": { "width": 1360, "height": 130 }, + "overrides": { + "componentConfig": { + "title": "์ถ”๊ฐ€ ์ •๋ณด", + "showHeader": true, + "padding": "md", + "borderStyle": "solid" + } + }, + "displayOrder": 22 + }, + { + "id": "input-memo", + "url": "@/lib/registry/components/v2-input", + "position": { "x": 40, "y": 735, "z": 3 }, + "size": { "width": 1320, "height": 70 }, + "overrides": { + "label": "๋ฉ”๋ชจ", + "columnName": "memo", + "type": "textarea", + "placeholder": "๋ฉ”๋ชจ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + }, + "displayOrder": 23 + }, + + { + "id": "btn-cancel", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1180, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ทจ์†Œ", + "webTypeConfig": { + "variant": "outline" + }, + "action": { + "type": "close" + } + }, + "displayOrder": 24 + }, + { + "id": "btn-save", + "url": "@/lib/registry/components/v2-button-primary", + "position": { "x": 1280, "y": 840, "z": 5 }, + "size": { "width": 90, "height": 40 }, + "overrides": { + "label": "์ €์žฅ", + "action": { + "type": "save" + } + }, + "displayOrder": 25 + } + ], + "gridSettings": { + "columns": 12, + "gap": 16, + "padding": 20, + "snapToGrid": true, + "showGrid": false + } +} diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 828d1aca..0ce2bae5 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -238,7 +238,8 @@ function ScreenViewPage() { compType?.includes("select") || compType?.includes("textarea") || compType?.includes("v2-input") || - compType?.includes("v2-select"); + compType?.includes("v2-select") || + compType?.includes("v2-media"); // ๐Ÿ†• ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ const hasColumnName = !!(comp as any).columnName; return isInputType && hasColumnName; }); diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 68fa0cb1..dbb1e923 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -13,6 +13,7 @@ import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; +import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; interface ScreenModalState { isOpen: boolean; @@ -322,12 +323,28 @@ export const ScreenModal: React.FC = ({ className }) => { try { setLoading(true); - // ํ™”๋ฉด ์ •๋ณด์™€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ - const [screenInfo, layoutData] = await Promise.all([ + // ํ™”๋ฉด ์ •๋ณด์™€ ๋ ˆ์ด์•„์›ƒ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (V2 API ์‚ฌ์šฉ์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’ ๋ณ‘ํ•ฉ) + const [screenInfo, v2LayoutData] = await Promise.all([ screenApi.getScreen(screenId), - screenApi.getLayout(screenId), + screenApi.getLayoutV2(screenId), ]); + // V2 โ†’ Legacy ๋ณ€ํ™˜ (๊ธฐ๋ณธ๊ฐ’ ๋ณ‘ํ•ฉ ํฌํ•จ) + let layoutData: any = null; + if (v2LayoutData && isValidV2Layout(v2LayoutData)) { + layoutData = convertV2ToLegacy(v2LayoutData); + if (layoutData) { + // screenResolution์€ V2 ๋ ˆ์ด์•„์›ƒ์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ + layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution; + } + } + + // V2 ๋ ˆ์ด์•„์›ƒ์ด ์—†์œผ๋ฉด ๊ธฐ์กด API๋กœ fallback + if (!layoutData) { + console.log("๐Ÿ“ฆ V2 ๋ ˆ์ด์•„์›ƒ ์—†์Œ, ๊ธฐ์กด API๋กœ fallback"); + layoutData = await screenApi.getLayout(screenId); + } + // ๐Ÿ†• URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ (์ˆ˜์ • ๋ชจ๋“œ) if (typeof window !== "undefined") { const urlParams = new URLSearchParams(window.location.search); @@ -604,23 +621,135 @@ export const ScreenModal: React.FC = ({ className }) => { transformOrigin: "center center", }} > - {screenData.components.map((component) => { + {(() => { + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ •์„ ์œ„ํ•ด ๋จผ์ € ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค ํŒŒ์•… + const isComponentHidden = (comp: any) => { + const cc = comp.componentConfig?.conditionalConfig || comp.conditionalConfig; + if (!cc?.enabled || !formData) return false; + + const { field, operator, value, action } = cc; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + return (action === "show" && !conditionMet) || (action === "hide" && conditionMet); + }; + + // ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ y ๋ฒ”์œ„ ์ˆ˜์ง‘ + const visibleRanges: { y: number; bottom: number }[] = []; + screenData.components.forEach((comp: any) => { + if (!isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + visibleRanges.push({ y, bottom: y + height }); + } + }); + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ์˜ "์‹ค์ œ ๋นˆ ๊ณต๊ฐ„" ๊ณ„์‚ฐ (ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜์ง€ ์•Š๋Š” ์˜์—ญ) + const getActualGap = (hiddenY: number, hiddenBottom: number): number => { + // ์ˆจ๊ฒจ์ง€๋Š” ์˜์—ญ ์ค‘ ํ‘œ์‹œ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + let gapStart = hiddenY; + let gapEnd = hiddenBottom; + + for (const visible of visibleRanges) { + // ๊ฒน์น˜๋Š” ์˜์—ญ ํ™•์ธ + if (visible.y < gapEnd && visible.bottom > gapStart) { + // ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธ + if (visible.y <= gapStart && visible.bottom >= gapEnd) { + // ์™„์ „ํžˆ ๋ฎํž˜ - ๋นˆ ๊ณต๊ฐ„ ์—†์Œ + return 0; + } else if (visible.y <= gapStart) { + // ์œ„์ชฝ์ด ๋ฎํž˜ + gapStart = visible.bottom; + } else if (visible.bottom >= gapEnd) { + // ์•„๋ž˜์ชฝ์ด ๋ฎํž˜ + gapEnd = visible.y; + } + } + } + + return Math.max(0, gapEnd - gapStart); + }; + + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์‹ค์ œ ๋นˆ ๊ณต๊ฐ„ ์ˆ˜์ง‘ + const hiddenGaps: { bottom: number; gap: number }[] = []; + screenData.components.forEach((comp: any) => { + if (isComponentHidden(comp)) { + const y = parseFloat(comp.position?.y?.toString() || "0"); + const height = parseFloat(comp.size?.height?.toString() || "0"); + const bottom = y + height; + const gap = getActualGap(y, bottom); + if (gap > 0) { + hiddenGaps.push({ bottom, gap }); + } + } + }); + + // bottom ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ ๋ฐ ์ค‘๋ณต ์ œ๊ฑฐ (๊ฐ™์€ bottom์€ ๊ฐ€์žฅ ํฐ gap๋งŒ ์œ ์ง€) + const mergedGaps = new Map(); + hiddenGaps.forEach(({ bottom, gap }) => { + const existing = mergedGaps.get(bottom) || 0; + mergedGaps.set(bottom, Math.max(existing, gap)); + }); + + const sortedGaps = Array.from(mergedGaps.entries()) + .map(([bottom, gap]) => ({ bottom, gap })) + .sort((a, b) => a.bottom - b.bottom); + + console.log('๐Ÿ” [Y์กฐ์ •] visibleRanges:', visibleRanges.filter(r => r.bottom - r.y > 50).map(r => `${r.y}~${r.bottom}`)); + console.log('๐Ÿ” [Y์กฐ์ •] hiddenGaps:', sortedGaps); + + // ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ y ์กฐ์ •๊ฐ’ ๊ณ„์‚ฐ ํ•จ์ˆ˜ + const getYOffset = (compY: number, compId?: string) => { + let offset = 0; + for (const { bottom, gap } of sortedGaps) { + // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆจ๊ฒจ์ง„ ์˜์—ญ ์•„๋ž˜์— ์žˆ์œผ๋ฉด ๊ทธ ๋นˆ ๊ณต๊ฐ„๋งŒํผ ์œ„๋กœ ์ด๋™ + if (compY > bottom) { + offset += gap; + } + } + if (offset > 0 && compId) { + console.log(`๐Ÿ” [Y์กฐ์ •] ${compId}: y=${compY} โ†’ ${compY - offset} (offset=${offset})`); + } + return offset; + }; + + return screenData.components.map((component: any) => { + // ์ˆจ๊ฒจ์ง€๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ Œ๋”๋ง ์•ˆํ•จ + if (isComponentHidden(component)) { + return null; + } + // ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ offset ์กฐ์ • ๋ถˆํ•„์š” const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; + + // ๐Ÿ†• ๋™์  y ์ขŒํ‘œ ์กฐ์ • (์ˆจ๊ฒจ์ง„ ์ปดํฌ๋„ŒํŠธ ๋†’์ด๋งŒํผ ์œ„๋กœ ์ด๋™) + const compY = parseFloat(component.position?.y?.toString() || "0"); + const yAdjustment = getYOffset(compY, component.id); // offset์ด 0์ด๋ฉด ์›๋ณธ ์œ„์น˜ ์‚ฌ์šฉ (ํ™”๋ฉด ๊ด€๋ฆฌ ํ•ด์ƒ๋„ ์‚ฌ์šฉ ์‹œ) - const adjustedComponent = - offsetX === 0 && offsetY === 0 - ? component - : { - ...component, - position: { - ...component.position, - x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, - }, - }; + const adjustedComponent = { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: compY - offsetY - yAdjustment, // ๐Ÿ†• ๋™์  ์กฐ์ • ์ ์šฉ + }, + }; return ( = ({ className }) => { companyCode={user?.companyCode} /> ); - })} + }); + })()}
diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 735fb53c..8dc5da89 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -335,13 +335,42 @@ export const InteractiveScreenViewerDynamic: React.FC { - // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ + // ์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ ํ‰๊ฐ€ (๊ธฐ์กด conditional ์‹œ์Šคํ…œ) const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents); // ์กฐ๊ฑด์— ๋”ฐ๋ผ ์ˆจ๊น€ ์ฒ˜๋ฆฌ if (!conditionalResult.visible) { return null; } + + // ๐Ÿ†• conditionalConfig ์‹œ์Šคํ…œ ์ฒดํฌ (V2 ๋ ˆ์ด์•„์›ƒ์šฉ) + const conditionalConfig = (comp as any).componentConfig?.conditionalConfig; + if (conditionalConfig?.enabled && formData) { + const { field, operator, value, action } = conditionalConfig; + const fieldValue = formData[field]; + + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + default: + conditionMet = fieldValue === value; + } + + if (action === "show" && !conditionMet) { + return null; + } + if (action === "hide" && conditionMet) { + return null; + } + } // ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ if (isDataTableComponent(comp)) { @@ -533,11 +562,26 @@ export const InteractiveScreenViewerDynamic: React.FC = {}; + + // v2-media ์ปดํฌ๋„ŒํŠธ์˜ columnName ๋ชฉ๋ก ์ˆ˜์ง‘ + const mediaColumnNames = new Set( + allComponents + .filter((c: any) => c.componentType === "v2-media" || c.url?.includes("v2-media")) + .map((c: any) => c.columnName || c.componentConfig?.columnName) + .filter(Boolean) + ); + Object.entries(formData).forEach(([key, value]) => { - // ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋Š” ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ด๋ฏ€๋กœ ์ œ์™ธ if (!Array.isArray(value)) { + // ๋ฐฐ์—ด์ด ์•„๋‹Œ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ์ €์žฅ masterFormData[key] = value; + } else if (mediaColumnNames.has(key)) { + // v2-media ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ์—ด์€ ์ฒซ ๋ฒˆ์งธ ๊ฐ’๋งŒ ์ €์žฅ (๋‹จ์ผ ํŒŒ์ผ ์ปฌ๋Ÿผ ๋Œ€์‘) + // ๋˜๋Š” JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ ค๋ฉด JSON.stringify(value) ์‚ฌ์šฉ + masterFormData[key] = value.length > 0 ? value[0] : null; + console.log(`๐Ÿ“ท ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ์ €์žฅ: ${key}, objid: ${masterFormData[key]}`); } else { console.log(`๐Ÿ”„ ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); } diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index c73e6598..192bd16c 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1623,55 +1623,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU }; }, [MIN_ZOOM, MAX_ZOOM]); - // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ ๋ฐ ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์Šค๋ƒ… + // ๊ฒฉ์ž ์„ค์ • ์—…๋ฐ์ดํŠธ (์ปดํฌ๋„ŒํŠธ ์ž๋™ ์กฐ์ • ์ œ๊ฑฐ๋จ) const updateGridSettings = useCallback( (newGridSettings: GridSettings) => { const newLayout = { ...layout, gridSettings: newGridSettings }; - - // ๊ฒฉ์ž ์Šค๋ƒ…์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒˆ๋กœ์šด ๊ฒฉ์ž์— ๋งž๊ฒŒ ์กฐ์ • - if (newGridSettings.snapToGrid && screenResolution.width > 0) { - // ์ƒˆ๋กœ์šด ๊ฒฉ์ž ์„ค์ •์œผ๋กœ ๊ฒฉ์ž ์ •๋ณด ์žฌ๊ณ„์‚ฐ (ํ•ด์ƒ๋„ ๊ธฐ์ค€) - const newGridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: newGridSettings.snapToGrid || false, - }); - - const gridUtilSettings = { - columns: newGridSettings.columns, - gap: newGridSettings.gap, - padding: newGridSettings.padding, - snapToGrid: true, // ํ•ญ์ƒ 10px ์Šค๋ƒ… ํ™œ์„ฑํ™” - }; - - const adjustedComponents = layout.components.map((comp) => { - const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); - const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); - - // gridColumns๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž๋™ ์กฐ์ • - let adjustedGridColumns = comp.gridColumns; - if (!adjustedGridColumns || adjustedGridColumns < 1 || adjustedGridColumns > newGridSettings.columns) { - adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); - } - - return { - ...comp, - position: snappedPosition, - size: snappedSize, - gridColumns: adjustedGridColumns, // gridColumns ์†์„ฑ ์ถ”๊ฐ€/์กฐ์ • - }; - }); - - newLayout.components = adjustedComponents; - // console.log("๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ ์œ„์น˜ ๋ฐ ํฌ๊ธฐ ์ž๋™ ์กฐ์ •:", adjustedComponents.length, "๊ฐœ"); - // console.log("์ƒˆ๋กœ์šด ๊ฒฉ์ž ์ •๋ณด:", newGridInfo); - } - + // ๐Ÿ†• ๊ฒฉ์ž ์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์ž๋™ ์กฐ์ • ๋กœ์ง ์ œ๊ฑฐ๋จ + // ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ "๊ฒฉ์ž ์žฌ์กฐ์ •" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์•ผ๋งŒ ์กฐ์ •๋จ setLayout(newLayout); saveToHistory(newLayout); }, - [layout, screenResolution, saveToHistory], + [layout, saveToHistory], ); // ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ/์œ„์น˜ ์œ ์ง€) diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index e49bf6d8..bcaaf054 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -58,6 +58,7 @@ export interface TableNodeData { label: string; subLabel?: string; isMain?: boolean; + isFilterTable?: boolean; // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”์ธ์ง€ (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ) isFocused?: boolean; // ํฌ์ปค์Šค๋œ ํ…Œ์ด๋ธ”์ธ์ง€ isFaded?: boolean; // ํ‘๋ฐฑ ์ฒ˜๋ฆฌํ• ์ง€ columns?: Array<{ @@ -448,7 +449,7 @@ const ScreenPreview: React.FC<{ layoutSummary: ScreenLayoutSummary; screenType: // ========== ํ…Œ์ด๋ธ” ๋…ธ๋“œ (ํ•˜๋‹จ) - ์ปฌ๋Ÿผ ๋ชฉ๋ก ํ‘œ์‹œ (์ปดํŒฉํŠธ) ========== export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { - const { label, subLabel, isMain, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data; + const { label, subLabel, isMain, isFilterTable, isFocused, isFaded, columns, highlightedColumns, joinColumns, joinColumnRefs, filterColumns, fieldMappings, referencedBy, saveInfos } = data; // ๊ฐ•์กฐํ•  ์ปฌ๋Ÿผ ์„ธํŠธ (์˜๋ฌธ ์ปฌ๋Ÿผ๋ช… ๊ธฐ์ค€) const highlightSet = new Set(highlightedColumns || []); @@ -574,16 +575,19 @@ export const TableNode: React.FC<{ data: TableNodeData }> = ({ data }) => { return (
{ + const handleScreenListRefresh = () => { + // refreshKey ์ฆ๊ฐ€๋กœ ๋ฐ์ดํ„ฐ ์žฌ๋กœ๋“œ ํŠธ๋ฆฌ๊ฑฐ + setRefreshKey(prev => prev + 1); + }; + + window.addEventListener("screen-list-refresh", handleScreenListRefresh); + return () => { + window.removeEventListener("screen-list-refresh", handleScreenListRefresh); + }; + }, []); + // ๊ทธ๋ฃน ๋˜๋Š” ํ™”๋ฉด์ด ๋ณ€๊ฒฝ๋  ๋•Œ ํฌ์ปค์Šค ์ดˆ๊ธฐํ™” useEffect(() => { setFocusedScreenId(null); @@ -170,6 +183,10 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId // ํ™”๋ฉด๋ณ„ ์‚ฌ์šฉ ์ปฌ๋Ÿผ ๋งคํ•‘ (ํ™”๋ฉด ID -> ํ…Œ์ด๋ธ”๋ช… -> ์‚ฌ์šฉ ์ปฌ๋Ÿผ๋“ค) const [screenUsedColumnsMap, setScreenUsedColumnsMap] = useState>>({}); + + // ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + // ์ด ๋ชฉ๋ก์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ + const [globalMainTables, setGlobalMainTables] = useState>(new Set()); // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ const loadTableColumns = useCallback( @@ -266,24 +283,26 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const flows = flowsRes.success ? flowsRes.data || [] : []; const relations = relationsRes.success ? relationsRes.data || [] : []; - // ๋ฐ์ดํ„ฐ ํ๋ฆ„์—์„œ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด๋“ค ์ถ”๊ฐ€ - flows.forEach((flow: any) => { - if (flow.source_screen_id === screen.screenId && flow.target_screen_id) { - const exists = screenList.some((s) => s.screenId === flow.target_screen_id); - if (!exists) { - screenList.push({ - screenId: flow.target_screen_id, - screenName: flow.target_screen_name || `ํ™”๋ฉด ${flow.target_screen_id}`, - screenCode: "", - tableName: "", - companyCode: screen.companyCode, - isActive: "Y", - createdDate: new Date(), - updatedDate: new Date(), - } as ScreenDefinition); + // ๋ฐ์ดํ„ฐ ํ๋ฆ„์—์„œ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด๋“ค ์ถ”๊ฐ€ (๊ฐœ๋ณ„ ํ™”๋ฉด ๋ชจ๋“œ์—์„œ๋งŒ - ๊ทธ๋ฃน ๋ชจ๋“œ์—์„œ๋Š” ๊ทธ๋ฃน ๋‚ด ํ™”๋ฉด๋งŒ ํ‘œ์‹œ) + if (!selectedGroup && screen) { + flows.forEach((flow: any) => { + if (flow.source_screen_id === screen.screenId && flow.target_screen_id) { + const exists = screenList.some((s) => s.screenId === flow.target_screen_id); + if (!exists) { + screenList.push({ + screenId: flow.target_screen_id, + screenName: flow.target_screen_name || `ํ™”๋ฉด ${flow.target_screen_id}`, + screenCode: "", + tableName: "", + companyCode: screen.companyCode, + isActive: "Y", + createdDate: new Date(), + updatedDate: new Date(), + } as ScreenDefinition); + } } - } - }); + }); + } // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์ •๋ณด ๋กœ๋“œ const screenIds = screenList.map((s) => s.screenId); @@ -305,6 +324,13 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId subTablesData = subTablesRes.data as Record; // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ (์กฐ์ธ ์ปฌ๋Ÿผ ์ •๋ณด ํฌํ•จ) setSubTablesDataMap(subTablesData); + + // ์ „์—ญ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์ €์žฅ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ์šฉ) + // ์ด ๋ชฉ๋ก์— ์žˆ๋Š” ํ…Œ์ด๋ธ”์€ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜๋˜์ง€ ์•Š์Œ + const globalMainTablesArr = (subTablesRes as any).globalMainTables as string[] | undefined; + if (globalMainTablesArr && Array.isArray(globalMainTablesArr)) { + setGlobalMainTables(new Set(globalMainTablesArr)); + } } } catch (e) { console.error("๋ ˆ์ด์•„์›ƒ ์š”์•ฝ/์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋กœ๋“œ ์‹คํŒจ:", e); @@ -434,9 +460,27 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId if (rel.table_name) mainTableSet.add(rel.table_name); }); - // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (componentConfig์—์„œ ์ถ”์ถœ๋œ ํ…Œ์ด๋ธ”๋“ค) - // ์„œ๋ธŒ ํ…Œ์ด๋ธ”์€ ๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”๋“ค - // ํ™”๋ฉด๋ณ„ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋งคํ•‘๋„ ํ•จ๊ป˜ ๊ตฌ์ถ• + // ============================================================ + // ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + // ============================================================ + // ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // 1. screen_definitions.table_name (์ปดํฌ๋„ŒํŠธ ์ง์ ‘ ์—ฐ๊ฒฐ) - ์ด๋ฏธ mainTableSet์— ์ถ”๊ฐ€๋จ + // 2. globalMainTables (WHERE ์กฐ๊ฑด ๋Œ€์ƒ, ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”) + // + // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์กฐ๊ฑด: + // - ์กฐ์ธ(JOIN)์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” (autocomplete ๋“ฑ์—์„œ ์ฐธ์กฐ) + // - ๋‹จ, mainTableSet์— ์žˆ์œผ๋ฉด ์ œ์™ธ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + + // 1. globalMainTables๋ฅผ mainTableSet์— ๋จผ์ € ์ถ”๊ฐ€ (์šฐ์„ ์ˆœ์œ„ ์ ์šฉ) + const filterTableSet = new Set(); // ๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”๋“ค + globalMainTables.forEach((tableName) => { + if (!mainTableSet.has(tableName)) { + mainTableSet.add(tableName); + filterTableSet.add(tableName); // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฅ˜ (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ) + } + }); + + // 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ˆ˜์ง‘ (mainTableSet์— ์—†๋Š” ๊ฒƒ๋งŒ) const newScreenSubTableMap: Record = {}; Object.entries(subTablesData).forEach(([screenIdStr, screenSubData]) => { @@ -444,11 +488,14 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId const subTableNames: string[] = []; screenSubData.subTables.forEach((subTable) => { - // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์—†๋Š” ๊ฒƒ๋งŒ ์„œ๋ธŒ ํ…Œ์ด๋ธ”๋กœ ์ถ”๊ฐ€ - if (!mainTableSet.has(subTable.tableName)) { - subTableSet.add(subTable.tableName); - subTableNames.push(subTable.tableName); + // mainTableSet์— ์žˆ์œผ๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ”์—์„œ ์ œ์™ธ (์šฐ์„ ์ˆœ์œ„: ๋ฉ”์ธ > ์„œ๋ธŒ) + if (mainTableSet.has(subTable.tableName)) { + return; } + + // ์กฐ์ธ์œผ๋กœ๋งŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” + subTableSet.add(subTable.tableName); + subTableNames.push(subTable.tableName); }); if (subTableNames.length > 0) { @@ -539,10 +586,19 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId isForeignKey: !!col.referenceTable || (col.columnName?.includes("_id") && col.columnName !== "id"), })); - // ์—ฌ๋Ÿฌ ํ™”๋ฉด์ด ๊ฐ™์€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉํ•˜๋ฉด "๊ณตํ†ต ๋ฉ”์ธ ํ…Œ์ด๋ธ”", ์•„๋‹ˆ๋ฉด "๋ฉ”์ธ ํ…Œ์ด๋ธ”" - const subLabel = linkedScreens.length > 1 - ? `๋ฉ”์ธ ํ…Œ์ด๋ธ” (${linkedScreens.length}๊ฐœ ํ™”๋ฉด)` - : "๋ฉ”์ธ ํ…Œ์ด๋ธ”"; + // ํ…Œ์ด๋ธ” ๋ถ„๋ฅ˜์— ๋”ฐ๋ฅธ ๋ผ๋ฒจ ๊ฒฐ์ • + // 1. ํ•„ํ„ฐ ํ…Œ์ด๋ธ” (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ์˜ ๋””ํ…Œ์ผ): "ํ•„ํ„ฐ ๋Œ€์ƒ ํ…Œ์ด๋ธ”" + // 2. ์—ฌ๋Ÿฌ ํ™”๋ฉด์ด ๊ฐ™์€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ: "๊ณตํ†ต ๋ฉ”์ธ ํ…Œ์ด๋ธ” (N๊ฐœ ํ™”๋ฉด)" + // 3. ์ผ๋ฐ˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”: "๋ฉ”์ธ ํ…Œ์ด๋ธ”" + const isFilterTable = filterTableSet.has(tableName); + let subLabel: string; + if (isFilterTable) { + subLabel = "ํ•„ํ„ฐ ๋Œ€์ƒ ํ…Œ์ด๋ธ” (๋งˆ์Šคํ„ฐ-๋””ํ…Œ์ผ)"; + } else if (linkedScreens.length > 1) { + subLabel = `๋ฉ”์ธ ํ…Œ์ด๋ธ” (${linkedScreens.length}๊ฐœ ํ™”๋ฉด)`; + } else { + subLabel = "๋ฉ”์ธ ํ…Œ์ด๋ธ”"; + } // ์ด ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜๋Š” ๊ด€๊ณ„๋“ค tableNodes.push({ @@ -552,7 +608,8 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId data: { label: tableName, subLabel: subLabel, - isMain: true, // mainTableSet์˜ ๋ชจ๋“  ํ…Œ์ด๋ธ”์€ ๋ฉ”์ธ + isMain: !isFilterTable, // ํ•„ํ„ฐ ํ…Œ์ด๋ธ”์€ isMain: false๋กœ ์„ค์ • (๋ณด๋ผ์ƒ‰ ํ…Œ๋‘๋ฆฌ ํ‘œ์‹œ์šฉ) + isFilterTable: isFilterTable, // ํ•„ํ„ฐ ํ…Œ์ด๋ธ” ์—ฌ๋ถ€ ํ‘œ์‹œ columns: formattedColumns, // referencedBy, filterColumns, saveInfos๋Š” styledNodes์—์„œ ํฌ์ปค์Šค ์ƒํƒœ์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ์„ค์ • }, diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index be0d265b..a843d710 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -822,8 +822,12 @@ export const V2PropertiesPanel: React.FC = ({
handleUpdate("style.labelText", e.target.value)} + value={selectedComponent.style?.labelText !== undefined ? selectedComponent.style.labelText : (selectedComponent.label || "")} + onChange={(e) => { + handleUpdate("style.labelText", e.target.value); + handleUpdate("label", e.target.value); // label๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + }} + placeholder="๋ผ๋ฒจ์„ ์ž…๋ ฅํ•˜์„ธ์š” (๋น„์šฐ๋ฉด ๋ผ๋ฒจ ์—†์Œ)" className="h-6 w-full px-2 py-0 text-xs" />
@@ -868,9 +872,9 @@ export const V2PropertiesPanel: React.FC = ({ )} - {/* ์˜ต์…˜ */} + {/* ์˜ต์…˜ - ์ž…๋ ฅ ํ•„๋“œ์—์„œ๋Š” ํ•ญ์ƒ ํ‘œ์‹œ, ๊ธฐํƒ€ ์ปดํฌ๋„ŒํŠธ๋Š” ์†์„ฑ์ด ์ •์˜๋œ ๊ฒฝ์šฐ๋งŒ ํ‘œ์‹œ */}
- {widget.required !== undefined && ( + {(isInputField || widget.required !== undefined) && (
= ({
)} - {widget.readonly !== undefined && ( + {(isInputField || widget.readonly !== undefined) && (
= ({
)} - {/* ์ˆจ๊น€ ์˜ต์…˜ */} + {/* ์ˆจ๊น€ ์˜ต์…˜ - ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์—์„œ ํ‘œ์‹œ */}
= ({ co placeholder: "", allowClear: false, maxSelections: undefined, + defaultValue: "", ...config, }; @@ -32,6 +34,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); const [newOption, setNewOption] = useState({ label: "", value: "" }); @@ -53,6 +56,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, allowClear: safeConfig.allowClear, maxSelections: safeConfig.maxSelections?.toString() || "", + defaultValue: safeConfig.defaultValue || "", }); setLocalOptions( @@ -68,6 +72,7 @@ export const SelectTypeConfigPanel: React.FC = ({ co safeConfig.placeholder, safeConfig.allowClear, safeConfig.maxSelections, + safeConfig.defaultValue, JSON.stringify(safeConfig.options), // ์˜ต์…˜ ๋ฐฐ์—ด์˜ ์ „์ฒด ๋‚ด์šฉ ๋ณ€ํ™” ๊ฐ์ง€ ]); @@ -174,6 +179,30 @@ export const SelectTypeConfigPanel: React.FC = ({ co />
+ {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} +
+ + +

ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์„ ํƒ๋  ๊ฐ’

+
+ {/* ๋‹ค์ค‘ ์„ ํƒ */}
)} -
+
{renderBiz()}
diff --git a/frontend/components/v2/V2Date.tsx b/frontend/components/v2/V2Date.tsx index c825b1bf..f3102d5a 100644 --- a/frontend/components/v2/V2Date.tsx +++ b/frontend/components/v2/V2Date.tsx @@ -460,7 +460,7 @@ export const V2Date = forwardRef((props, ref) => { className="flex flex-col" style={{ width: componentWidth, - height: componentHeight, + // ๐Ÿ”ง ๋†’์ด๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•„๋‹Œ ์ž…๋ ฅ ํ•„๋“œ์—๋งŒ ์ ์šฉ (๋ผ๋ฒจ ๋†’์ด๋Š” ๋ณ„๋„) }} > {showLabel && ( @@ -478,7 +478,14 @@ export const V2Date = forwardRef((props, ref) => { {required && *} )} -
{renderDatePicker()}
+
+ {renderDatePicker()} +
); }); diff --git a/frontend/components/v2/V2Hierarchy.tsx b/frontend/components/v2/V2Hierarchy.tsx index 17a3e141..23e4fd85 100644 --- a/frontend/components/v2/V2Hierarchy.tsx +++ b/frontend/components/v2/V2Hierarchy.tsx @@ -469,7 +469,7 @@ export const V2Hierarchy = forwardRef( className="flex flex-col" style={{ width: componentWidth, - height: componentHeight, + // ๐Ÿ”ง ๋†’์ด๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•„๋‹Œ ์ž…๋ ฅ ํ•„๋“œ์—๋งŒ ์ ์šฉ (๋ผ๋ฒจ ๋†’์ด๋Š” ๋ณ„๋„) }} > {showLabel && ( @@ -487,7 +487,12 @@ export const V2Hierarchy = forwardRef( {required && *} )} -
+
{renderHierarchy()}
diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index edaefffb..c19b3820 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -361,8 +361,17 @@ export const V2Input = forwardRef((props, ref) => const [isGeneratingNumbering, setIsGeneratingNumbering] = useState(false); const hasGeneratedNumberingRef = useRef(false); - // tableName ์ถ”์ถœ (props์—์„œ ์ „๋‹ฌ๋ฐ›๊ฑฐ๋‚˜ config์—์„œ) - const tableName = (props as any).tableName || (config as any).tableName; + // tableName ์ถ”์ถœ (์—ฌ๋Ÿฌ ์†Œ์Šค์—์„œ ํ™•์ธ) + // 1. props์—์„œ ์ง์ ‘ ์ „๋‹ฌ๋ฐ›์€ ๊ฐ’ + // 2. config์—์„œ ์„ค์ •๋œ ๊ฐ’ + // 3. ์ปดํฌ๋„ŒํŠธ overrides์—์„œ ์„ค์ •๋œ ๊ฐ’ (V2 ๋ ˆ์ด์•„์›ƒ) + // 4. screenInfo์—์„œ ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… + const tableName = + (props as any).tableName || + (config as any).tableName || + (props as any).component?.tableName || + (props as any).component?.overrides?.tableName || + (props as any).screenInfo?.tableName; // ์ˆ˜์ • ๋ชจ๋“œ ์—ฌ๋ถ€ ํ™•์ธ const originalData = (props as any).originalData || (props as any)._originalData; @@ -445,8 +454,10 @@ export const V2Input = forwardRef((props, ref) => // formData์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ๊ฐ’ ์ถ”์ถœ (์ฑ„๋ฒˆ ํŒŒํŠธ์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ ์‹œ) // ์ฑ„๋ฒˆ ํ•„๋“œ ์ž์ฒด์˜ ๊ฐ’์€ ์ œ์™ธํ•ด์•ผ ํ•จ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) + // inputType์„ ์—ฌ๋Ÿฌ ์†Œ์Šค์—์„œ ํ™•์ธ + const propsInputType = (props as any).inputType; const categoryValuesForNumbering = useMemo(() => { - const inputType = config.inputType || config.type || "text"; + const inputType = propsInputType || config.inputType || config.type || "text"; if (inputType !== "numbering") return ""; // formData์—์„œ category ํƒ€์ž… ํ•„๋“œ ๊ฐ’๋“ค์„ ์ถ”์ถœ (์ฑ„๋ฒˆ ํ•„๋“œ ์ž์ฒด๋Š” ์ œ์™ธ) const categoryFields: Record = {}; @@ -458,12 +469,13 @@ export const V2Input = forwardRef((props, ref) => } } return JSON.stringify(categoryFields); - }, [config.inputType, config.type, formData, columnName]); + }, [propsInputType, config.inputType, config.type, formData, columnName]); // ์ฑ„๋ฒˆ ํƒ€์ž… ์ž๋™์ƒ์„ฑ ๋กœ์ง (ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ์—์„œ ์„ค์ •๋œ numberingRuleId ์‚ฌ์šฉ) useEffect(() => { const generateNumberingCode = async () => { - const inputType = config.inputType || config.type || "text"; + // inputType์„ ์—ฌ๋Ÿฌ ์†Œ์Šค์—์„œ ํ™•์ธ (props์—์„œ ์ง์ ‘ ์ „๋‹ฌ๋ฐ›๊ฑฐ๋‚˜ config์—์„œ) + const inputType = (props as any).inputType || config.inputType || config.type || "text"; // numbering ํƒ€์ž…์ด ์•„๋‹ˆ๋ฉด ์Šคํ‚ต if (inputType !== "numbering") { @@ -524,9 +536,12 @@ export const V2Input = forwardRef((props, ref) => } // detailSettings์—์„œ numberingRuleId ์ถ”์ถœ - if (targetColumn.detailSettings && typeof targetColumn.detailSettings === "string") { + if (targetColumn.detailSettings) { try { - const parsed = JSON.parse(targetColumn.detailSettings); + // ๋ฌธ์ž์—ด์ด๋ฉด ํŒŒ์‹ฑ, ๊ฐ์ฒด๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + const parsed = typeof targetColumn.detailSettings === "string" + ? JSON.parse(targetColumn.detailSettings) + : targetColumn.detailSettings; numberingRuleIdRef.current = parsed.numberingRuleId || null; // ๐Ÿ†• ์ฑ„๋ฒˆ ๊ทœ์น™ ID๋ฅผ formData์— ์ €์žฅ (์ €์žฅ ์‹œ allocateCode ํ˜ธ์ถœ์„ ์œ„ํ•ด) @@ -618,7 +633,7 @@ export const V2Input = forwardRef((props, ref) => // ํƒ€์ž…๋ณ„ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง const renderInput = () => { - const inputType = config.inputType || config.type || "text"; + const inputType = propsInputType || config.inputType || config.type || "text"; switch (inputType) { case "text": return ( @@ -799,7 +814,7 @@ export const V2Input = forwardRef((props, ref) => className="flex flex-col" style={{ width: componentWidth, - height: componentHeight, + // ๐Ÿ”ง ๋†’์ด๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•„๋‹Œ ์ž…๋ ฅ ํ•„๋“œ์—๋งŒ ์ ์šฉ (๋ผ๋ฒจ ๋†’์ด๋Š” ๋ณ„๋„) }} > {showLabel && ( @@ -817,7 +832,14 @@ export const V2Input = forwardRef((props, ref) => {required && *} )} -
{renderInput()}
+
+ {renderInput()} +
); }); diff --git a/frontend/components/v2/V2Media.tsx b/frontend/components/v2/V2Media.tsx index f7359962..6a154863 100644 --- a/frontend/components/v2/V2Media.tsx +++ b/frontend/components/v2/V2Media.tsx @@ -10,12 +10,13 @@ * - audio: ์˜ค๋””์˜ค */ -import React, { forwardRef, useCallback, useRef, useState } from "react"; +import React, { forwardRef, useCallback, useRef, useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { V2MediaProps } from "@/types/v2-components"; -import { Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2 } from "lucide-react"; +import { Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2, Plus } from "lucide-react"; +import { apiClient } from "@/lib/api/client"; /** * ํŒŒ์ผ ํฌ๊ธฐ ํฌ๋งทํŒ… @@ -57,15 +58,42 @@ const FileUploader = forwardRef { const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); + // ์—…๋กœ๋“œ ์งํ›„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์œ„ํ•œ ๋กœ์ปฌ ์ƒํƒœ + const [localPreviewUrls, setLocalPreviewUrls] = useState([]); - const files = Array.isArray(value) ? value : value ? [value] : []; + // objid๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL๋กœ ๋ณ€ํ™˜ + const toPreviewUrl = (val: any): string => { + if (!val) return ""; + const strVal = String(val); + if (strVal.startsWith("/") || strVal.startsWith("http")) return strVal; + if (/^\d+$/.test(strVal)) return `/api/files/preview/${strVal}`; + return strVal; + }; + + // value๋ฅผ URL ํ˜•ํƒœ์˜ files ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ + const rawFiles = Array.isArray(value) ? value : value ? [value] : []; + const filesFromValue = rawFiles.map(toPreviewUrl).filter(Boolean); + + console.log("[FileUploader] value:", value, "rawFiles:", rawFiles, "filesFromValue:", filesFromValue, "localPreviewUrls:", localPreviewUrls); + + // value๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋กœ์ปฌ ์ƒํƒœ ์ดˆ๊ธฐํ™” + useEffect(() => { + if (filesFromValue.length > 0) { + setLocalPreviewUrls([]); + } + }, [filesFromValue.length]); + + // ์ตœ์ข… files: value์—์„œ ์˜จ ํŒŒ์ผ + ๋กœ์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ค‘๋ณต ์ œ๊ฑฐ) + const files = filesFromValue.length > 0 ? filesFromValue : localPreviewUrls; + + console.log("[FileUploader] final files:", files); // ํŒŒ์ผ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ const handleFileSelect = useCallback(async (selectedFiles: FileList | null) => { @@ -89,36 +117,53 @@ const FileUploader = forwardRef 0) { + const uploadedFile = data.files[0]; + const objid = String(uploadedFile.objid); + uploadedUrls.push(objid); + // ์ฆ‰์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์œ„ํ•ด ๋กœ์ปฌ ์ƒํƒœ์— URL ์ €์žฅ + const previewUrl = `/api/files/preview/${objid}`; + setLocalPreviewUrls(prev => multiple ? [...prev, previewUrl] : [previewUrl]); + } else if (data.objid) { + const objid = String(data.objid); + uploadedUrls.push(objid); + const previewUrl = `/api/files/preview/${objid}`; + setLocalPreviewUrls(prev => multiple ? [...prev, previewUrl] : [previewUrl]); + } else if (data.url) { uploadedUrls.push(data.url); + setLocalPreviewUrls(prev => multiple ? [...prev, data.url] : [data.url]); } else if (data.filePath) { uploadedUrls.push(data.filePath); + setLocalPreviewUrls(prev => multiple ? [...prev, data.filePath] : [data.filePath]); } } if (multiple) { - onChange?.([...files, ...uploadedUrls]); + const newValue = [...filesFromValue, ...uploadedUrls]; + console.log("[FileUploader] onChange called with:", newValue); + onChange?.(newValue); } else { - onChange?.(uploadedUrls[0] || ""); + const newValue = uploadedUrls[0] || ""; + console.log("[FileUploader] onChange called with:", newValue); + onChange?.(newValue); } } catch (err) { setError(err instanceof Error ? err.message : "์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"); } finally { setIsUploading(false); } - }, [files, multiple, maxSize, uploadEndpoint, onChange]); + }, [filesFromValue, multiple, maxSize, uploadEndpoint, onChange]); // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ const handleDragOver = useCallback((e: React.DragEvent) => { @@ -139,21 +184,33 @@ const FileUploader = forwardRef { - const newFiles = files.filter((_, i) => i !== index); + // ๋กœ์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋„ ์‚ญ์ œ + setLocalPreviewUrls(prev => prev.filter((_, i) => i !== index)); + // value์—์„œ ์˜จ ํŒŒ์ผ ์‚ญ์ œ + const newFiles = filesFromValue.filter((_, i) => i !== index); onChange?.(multiple ? newFiles : ""); - }, [files, multiple, onChange]); + }, [filesFromValue, multiple, onChange]); + + // ์ฒซ ๋ฒˆ์งธ ํŒŒ์ผ์ด ์ด๋ฏธ์ง€์ธ์ง€ ํ™•์ธ + const firstFile = files[0]; + const isFirstFileImage = firstFile && ( + /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(firstFile) || + firstFile.includes("/preview/") || + firstFile.includes("/api/files/preview/") + ); return ( -
- {/* ์—…๋กœ๋“œ ์˜์—ญ */} +
+ {/* ๋ฉ”์ธ ์—…๋กœ๋“œ ๋ฐ•์Šค - ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ•์Šค ์•ˆ์— ํ‘œ์‹œ */}
!disabled && inputRef.current?.click()} + onClick={() => !disabled && !firstFile && inputRef.current?.click()} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} @@ -168,13 +225,64 @@ const FileUploader = forwardRef - {isUploading ? ( -
+ {firstFile ? ( + // ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ๋ฐ•์Šค ์•ˆ์— ํ‘œ์‹œ +
+ {isFirstFileImage ? ( + // ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ + ) : ( + // ์ผ๋ฐ˜ ํŒŒ์ผ +
+ + + {firstFile.split("/").pop()} + +
+ )} + {/* ํ˜ธ๋ฒ„ ์‹œ ์•ก์…˜ ๋ฒ„ํŠผ */} +
+ {isFirstFileImage && ( + + )} + + +
+
+ ) : isUploading ? ( +
์—…๋กœ๋“œ ์ค‘...
) : ( -
+
ํด๋ฆญ @@ -193,26 +301,45 @@ const FileUploader = forwardRef{error}
)} - {/* ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋ชฉ๋ก */} - {files.length > 0 && ( -
- {files.map((file, index) => ( -
- - {file.split("/").pop()} - -
- ))} + {/* ์ถ”๊ฐ€ ํŒŒ์ผ ๋ชฉ๋ก (multiple์ผ ๋•Œ 2๋ฒˆ์งธ ํŒŒ์ผ๋ถ€ํ„ฐ) */} + {multiple && files.length > 1 && ( +
+ {files.slice(1).map((file, index) => { + const isImage = /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(file) || + file.includes("/preview/") || + file.includes("/api/files/preview/"); + + return ( +
+ {isImage ? ( + {`ํŒŒ์ผ + ) : ( + + )} +
+ +
+
+ ); + })} + {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} +
!disabled && inputRef.current?.click()} + > + +
)}
@@ -241,7 +368,7 @@ const ImageUploader = forwardRef { const inputRef = useRef(null); @@ -249,7 +376,18 @@ const ImageUploader = forwardRef(null); - const images = Array.isArray(value) ? value : value ? [value] : []; + // objid๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL๋กœ ๋ณ€ํ™˜ + const toPreviewUrl = (val: any): string => { + if (!val) return ""; + const strVal = String(val); + if (strVal.startsWith("/") || strVal.startsWith("http")) return strVal; + if (/^\d+$/.test(strVal)) return `/api/files/preview/${strVal}`; + return strVal; + }; + + // value๋ฅผ URL ํ˜•ํƒœ์˜ images ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ + const rawImages = Array.isArray(value) ? value : value ? [value] : []; + const images = rawImages.map(toPreviewUrl).filter(Boolean); // ํŒŒ์ผ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ const handleFileSelect = useCallback(async (selectedFiles: FileList | null) => { @@ -270,20 +408,30 @@ const ImageUploader = forwardRef 0) { + const uploadedFile = data.files[0]; + // objid๋งŒ ์ €์žฅ (DB ์ €์žฅ์šฉ) - ํ‘œ์‹œ๋Š” V2MediaRenderer์—์„œ URL๋กœ ๋ณ€ํ™˜ + uploadedUrls.push(String(uploadedFile.objid)); + } else if (data.objid) { + uploadedUrls.push(String(data.objid)); + } else if (data.url) { uploadedUrls.push(data.url); } else if (data.filePath) { uploadedUrls.push(data.filePath); } + } catch (err) { + console.error("์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹คํŒจ:", err); } } @@ -304,82 +452,126 @@ const ImageUploader = forwardRef - {/* ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} - {preview && images.length > 0 && ( -
- {images.map((src, index) => ( -
+
+ {/* ๋ฉ”์ธ ์—…๋กœ๋“œ ๋ฐ•์Šค - ์ฒซ ๋ฒˆ์งธ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ•์Šค ์•ˆ์— ํ‘œ์‹œ */} +
!disabled && !mainImage && inputRef.current?.click()} + onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} + onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} + onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFileSelect(e.dataTransfer.files); }} + > + handleFileSelect(e.target.files)} + className="hidden" + /> + + {mainImage ? ( + // ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ•์Šค ์•ˆ์— ํ‘œ์‹œ +
+ ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ + {/* ํ˜ธ๋ฒ„ ์‹œ ์•ก์…˜ ๋ฒ„ํŠผ */} +
+ + + +
+
+ ) : isUploading ? ( +
+
+ ์—…๋กœ๋“œ ์ค‘... +
+ ) : ( +
+ + + ํด๋ฆญ ๋˜๋Š” ํŒŒ์ผ์„ ๋“œ๋ž˜๊ทธํ•˜์„ธ์š” + + + ์ตœ๋Œ€ {Math.round(maxSize / 1024 / 1024)} MB (*/*) + +
+ )} +
+ + {/* ์ถ”๊ฐ€ ์ด๋ฏธ์ง€ ๋ชฉ๋ก (multiple์ผ ๋•Œ๋งŒ) */} + {multiple && additionalImages.length > 0 && ( +
+ {additionalImages.map((src, index) => ( +
{`์ด๋ฏธ์ง€ -
- +
))} -
- )} - - {/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ */} - {(!images.length || multiple) && ( -
!disabled && inputRef.current?.click()} - onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} - onDragLeave={(e) => { e.preventDefault(); setIsDragging(false); }} - onDrop={(e) => { e.preventDefault(); setIsDragging(false); handleFileSelect(e.dataTransfer.files); }} - > - handleFileSelect(e.target.files)} - className="hidden" - /> - - {isUploading ? ( -
-
- ์—…๋กœ๋“œ ์ค‘... -
- ) : ( -
- - - ์ด๋ฏธ์ง€ {multiple ? "์ถ”๊ฐ€" : "์„ ํƒ"} - -
- )} + {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} +
!disabled && inputRef.current?.click()} + > + +
)}
@@ -473,6 +665,22 @@ export const V2Media = forwardRef( // config๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ const config = configProp || { type: "image" as const }; + + // objid๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ + const toPreviewUrl = (val: any): string => { + if (!val) return ""; + const strVal = String(val); + if (strVal.startsWith("/") || strVal.startsWith("http")) return strVal; + if (/^\d+$/.test(strVal)) return `/api/files/preview/${strVal}`; + return strVal; + }; + + // value๋ฅผ URL๋กœ ๋ณ€ํ™˜ (๋ฐฐ์—ด ๋˜๋Š” ๋‹จ์ผ ๊ฐ’) + const convertedValue = Array.isArray(value) + ? value.map(toPreviewUrl) + : value ? toPreviewUrl(value) : value; + + console.log("[V2Media] original value:", value, "-> converted:", convertedValue, "onChange:", typeof onChange); // ํƒ€์ž…๋ณ„ ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง const renderMedia = () => { @@ -483,7 +691,7 @@ export const V2Media = forwardRef( case "file": return ( ( case "image": return ( (
{showLabel && ( @@ -561,7 +769,12 @@ export const V2Media = forwardRef( {required && *} )} -
+
{renderMedia()}
diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 872945f9..1aeac80d 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -751,7 +751,7 @@ export const V2Select = forwardRef( className="flex flex-col" style={{ width: componentWidth, - height: componentHeight, + // ๐Ÿ”ง ๋†’์ด๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•„๋‹Œ ์ž…๋ ฅ ํ•„๋“œ์—๋งŒ ์ ์šฉ (๋ผ๋ฒจ ๋†’์ด๋Š” ๋ณ„๋„) }} > {showLabel && ( @@ -769,7 +769,12 @@ export const V2Select = forwardRef( {required && *} )} -
+
{renderSelect()}
diff --git a/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx b/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx index 7092d4ab..056facac 100644 --- a/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SelectConfigPanel.tsx @@ -194,6 +194,32 @@ export const V2SelectConfigPanel: React.FC = ({

)}
+ + {/* ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • */} + {options.length > 0 && ( +
+ + +

+ ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž๋™ ์„ ํƒ๋  ๊ฐ’ +

+
+ )}
)} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 088b0644..9571abef 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -207,6 +207,88 @@ export const DynamicComponentRenderer: React.FC = // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ๋ณ€ํ™˜ ์™„๋ฃŒ + // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์ฒดํฌ (conditionalConfig) + // componentConfig ๋˜๋Š” overrides์—์„œ conditionalConfig๋ฅผ ๊ฐ€์ ธ์™€์„œ formData์™€ ๋น„๊ต + const conditionalConfig = (component as any).componentConfig?.conditionalConfig || (component as any).overrides?.conditionalConfig; + + // ๋””๋ฒ„๊ทธ: ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์„ค์ • ํ™•์ธ + if (conditionalConfig?.enabled) { + console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id}:`, { + conditionalConfig, + formData: props.formData, + hasFormData: !!props.formData + }); + } + + if (conditionalConfig?.enabled && props.formData) { + const { field, operator, value, action } = conditionalConfig; + const fieldValue = props.formData[field]; + + console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ํ‰๊ฐ€] ${component.id}:`, { + field, + fieldValue, + operator, + expectedValue: value, + action + }); + + // ์กฐ๊ฑด ํ‰๊ฐ€ + let conditionMet = false; + switch (operator) { + case "=": + case "==": + case "===": + conditionMet = fieldValue === value; + break; + case "!=": + case "!==": + conditionMet = fieldValue !== value; + break; + case ">": + conditionMet = Number(fieldValue) > Number(value); + break; + case "<": + conditionMet = Number(fieldValue) < Number(value); + break; + case ">=": + conditionMet = Number(fieldValue) >= Number(value); + break; + case "<=": + conditionMet = Number(fieldValue) <= Number(value); + break; + case "contains": + conditionMet = String(fieldValue || "").includes(String(value)); + break; + case "empty": + conditionMet = !fieldValue || fieldValue === ""; + break; + case "notEmpty": + conditionMet = !!fieldValue && fieldValue !== ""; + break; + default: + conditionMet = fieldValue === value; + } + + // ์•ก์…˜์— ๋”ฐ๋ผ ๋ Œ๋”๋ง ๊ฒฐ์ • + console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ] ${component.id}:`, { + conditionMet, + action, + shouldRender: action === "show" ? conditionMet : !conditionMet + }); + + if (action === "show" && !conditionMet) { + // "show" ์•ก์…˜: ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜์ง€ ์•Š์œผ๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ + console.log(`โŒ [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id} ์ˆจ๊น€ ์ฒ˜๋ฆฌ (show ์กฐ๊ฑด ๋ถˆ์ถฉ์กฑ)`); + return null; + } + if (action === "hide" && conditionMet) { + // "hide" ์•ก์…˜: ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ + console.log(`โŒ [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id} ์ˆจ๊น€ ์ฒ˜๋ฆฌ (hide ์กฐ๊ฑด ์ถฉ์กฑ)`); + return null; + } + // "enable"/"disable" ์•ก์…˜์€ conditionalDisabled props๋กœ ์ „๋‹ฌ + } + // ๐Ÿ†• ๋ชจ๋“  v2- ์ปดํฌ๋„ŒํŠธ๋Š” ComponentRegistry์—์„œ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ // (v2-input, v2-select, v2-repeat-container ๋“ฑ ๋ชจ๋‘ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ) @@ -343,7 +425,19 @@ export const DynamicComponentRenderer: React.FC = const safeProps = filterDOMProps(restProps); // ์ปดํฌ๋„ŒํŠธ์˜ columnName์— ํ•ด๋‹นํ•˜๋Š” formData ๊ฐ’ ์ถ”์ถœ - const fieldName = (component as any).columnName || component.id; + const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id; + + // ๐Ÿ” V2Media ๋””๋ฒ„๊น… + if (componentType === "v2-media") { + console.log("[DynamicComponentRenderer] v2-media:", { + componentId: component.id, + columnName: (component as any).columnName, + configColumnName: (component as any).componentConfig?.columnName, + fieldName, + formDataValue: props.formData?.[fieldName], + formDataKeys: props.formData ? Object.keys(props.formData) : [] + }); + } // ๋‹ค์ค‘ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™” let currentValue; @@ -412,10 +506,12 @@ export const DynamicComponentRenderer: React.FC = }; // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ (ํ™”๋ฉด ํ…Œ์ด๋ธ”์ด ์•„๋‹Œ ๊ฒ€์ƒ‰ ๋Œ€์ƒ ํ…Œ์ด๋ธ”) + // ๐Ÿ†• v2-input๋„ ํฌํ•จ (์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ ์‹œ tableName ํ•„์š”) const useConfigTableName = componentType === "entity-search-input" || componentType === "autocomplete-search-input" || - componentType === "modal-repeater-table"; + componentType === "modal-repeater-table" || + componentType === "v2-input"; const rendererProps = { component, @@ -430,9 +526,21 @@ export const DynamicComponentRenderer: React.FC = componentConfig: component.componentConfig, // componentConfig์˜ ๋ชจ๋“  ์†์„ฑ์„ props๋กœ spread (tableName, displayField ๋“ฑ) ...(component.componentConfig || {}), + // ๐Ÿ†• V2 ๋ ˆ์ด์•„์›ƒ์—์„œ overrides์—์„œ ๋ณต์›๋œ ์ƒ์œ„ ๋ ˆ๋ฒจ ์†์„ฑ๋“ค๋„ ์ „๋‹ฌ + inputType: (component as any).inputType || component.componentConfig?.inputType, + columnName: (component as any).columnName || component.componentConfig?.columnName, value: currentValue, // formData์—์„œ ์ถ”์ถœํ•œ ํ˜„์žฌ ๊ฐ’ ์ „๋‹ฌ // ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค ์ „๋‹ฌ - autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration, + // ๐Ÿ†• webTypeConfig.numberingRuleId๊ฐ€ ์žˆ์œผ๋ฉด autoGeneration์œผ๋กœ ๋ณ€ํ™˜ + autoGeneration: component.autoGeneration || + component.componentConfig?.autoGeneration || + ((component as any).webTypeConfig?.numberingRuleId ? { + type: "numbering_rule" as const, + enabled: true, + options: { + numberingRuleId: (component as any).webTypeConfig.numberingRuleId, + }, + } : undefined), hidden: hiddenValue, // React ์ „์šฉ props๋“ค์€ ์ง์ ‘ ์ „๋‹ฌ (DOM์— ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ) isInteractive, @@ -440,7 +548,10 @@ export const DynamicComponentRenderer: React.FC = onFormDataChange, onChange: handleChange, // ๊ฐœ์„ ๋œ onChange ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌ // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ๋Š” componentConfig.tableName ์œ ์ง€, ๊ทธ ์™ธ๋Š” ํ™”๋ฉด ํ…Œ์ด๋ธ”๋ช… ์‚ฌ์šฉ - tableName: useConfigTableName ? component.componentConfig?.tableName || tableName : tableName, + // ๐Ÿ†• component.tableName๋„ ํ™•์ธ (V2 ๋ ˆ์ด์•„์›ƒ์—์„œ overrides.tableName์ด ๋ณต์›๋จ) + tableName: useConfigTableName + ? component.componentConfig?.tableName || (component as any).tableName || tableName + : tableName, menuId, // ๐Ÿ†• ๋ฉ”๋‰ด ID menuObjid, // ๐Ÿ†• ๋ฉ”๋‰ด OBJID (๋ฉ”๋‰ด ์Šค์ฝ”ํ”„) selectedScreen, // ๐Ÿ†• ํ™”๋ฉด ์ •๋ณด diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 4aa4d930..7c6470fa 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -654,11 +654,11 @@ export function RepeaterTable({
{/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - {/* ์ฒดํฌ๋ฐ•์Šค ํ—ค๋” - ์ขŒ์ธก ๊ณ ์ • */} - - {visibleColumns.map((col) => { + {visibleColumns.map((col, colIndex) => { const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; const activeOption = hasDynamicSource @@ -677,7 +677,7 @@ export function RepeaterTable({ return ( {data.length === 0 ? ( - + {/* ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ๋“ค */} - {visibleColumns.map((col) => ( + {visibleColumns.map((col, colIndex) => ( {showRowNumber && ( - )} {columns.map((col) => ( ))} {!readOnly && allowDelete && ( - )} @@ -707,8 +707,9 @@ export function SimpleRepeaterTableComponent({ {value.length === 0 ? ( - + ) : ( value.map((row, rowIndex) => ( - + {showRowNumber && ( - )} {columns.map((col) => ( - ))} {!readOnly && allowDelete && ( -
+ ์ˆœ์„œ + handleDoubleClick(col.field)} @@ -765,8 +765,9 @@ export function RepeaterTable({
@@ -787,6 +788,7 @@ export function RepeaterTable({ <> {/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค - ์ขŒ์ธก ๊ณ ์ • */} {/* ์ฒดํฌ๋ฐ•์Šค - ์ขŒ์ธก ๊ณ ์ • */}
+ # @@ -699,7 +699,7 @@ export function SimpleRepeaterTableComponent({ + ์‚ญ์ œ
@@ -724,19 +725,19 @@ export function SimpleRepeaterTableComponent({
+ {rowIndex + 1} + {renderCell(row, col, rowIndex)} +