From 772514c270228735eddc77fb6500f088e9d0dbea Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 5 Mar 2026 09:59:23 +0900 Subject: [PATCH 1/6] Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node --- .../src/controllers/adminController.ts | 2 + backend-node/src/services/menuCopyService.ts | 197 ++++++++++++++---- docker/dev/docker-compose.backend.mac.yml | 2 +- scripts/menu-copy-automation.ts | 161 ++++++++++++++ 4 files changed, 316 insertions(+), 46 deletions(-) create mode 100644 scripts/menu-copy-automation.ts diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index 6cc62cc6..57edad10 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -3690,6 +3690,8 @@ export async function copyMenu( ? { removeText: req.body.screenNameConfig.removeText, addPrefix: req.body.screenNameConfig.addPrefix, + replaceFrom: req.body.screenNameConfig.replaceFrom, + replaceTo: req.body.screenNameConfig.replaceTo, } : undefined; diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index aee32eeb..747d5427 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -373,7 +373,8 @@ export class MenuCopyService { private async collectScreens( menuObjids: number[], sourceCompanyCode: string, - client: PoolClient + client: PoolClient, + menus?: Menu[] ): Promise> { logger.info( `๐Ÿ“„ ํ™”๋ฉด ์ˆ˜์ง‘ ์‹œ์ž‘: ${menuObjids.length}๊ฐœ ๋ฉ”๋‰ด, company=${sourceCompanyCode}` @@ -394,9 +395,25 @@ export class MenuCopyService { screenIds.add(assignment.screen_id); } - logger.info(`๐Ÿ“Œ ์ง์ ‘ ํ• ๋‹น ํ™”๋ฉด: ${screenIds.size}๊ฐœ`); + // 1.5) menu_url์—์„œ ์ฐธ์กฐ๋˜๋Š” ํ™”๋ฉด ์ˆ˜์ง‘ (/screens/{screenId} ํŒจํ„ด) + if (menus) { + const screenIdPattern = /\/screens\/(\d+)/; + for (const menu of menus) { + if (menu.menu_url) { + const match = menu.menu_url.match(screenIdPattern); + if (match) { + const urlScreenId = parseInt(match[1], 10); + if (!isNaN(urlScreenId) && urlScreenId > 0) { + screenIds.add(urlScreenId); + } + } + } + } + } - // 2) ํ™”๋ฉด ๋‚ด๋ถ€์—์„œ ์ฐธ์กฐ๋˜๋Š” ํ™”๋ฉด (์žฌ๊ท€) + logger.info(`๐Ÿ“Œ ์ง์ ‘ ํ• ๋‹น + menu_url ํ™”๋ฉด: ${screenIds.size}๊ฐœ`); + + // 2) ํ™”๋ฉด ๋‚ด๋ถ€์—์„œ ์ฐธ์กฐ๋˜๋Š” ํ™”๋ฉด (์žฌ๊ท€) - V1 + V2 ๋ ˆ์ด์•„์›ƒ ๋ชจ๋‘ ํƒ์ƒ‰ const queue = Array.from(screenIds); while (queue.length > 0) { @@ -405,17 +422,29 @@ export class MenuCopyService { if (visited.has(screenId)) continue; visited.add(screenId); - // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const referencedScreens: number[] = []; + + // V1 ๋ ˆ์ด์•„์›ƒ์—์„œ ์ฐธ์กฐ ํ™”๋ฉด ์ถ”์ถœ const layoutsResult = await client.query( `SELECT * FROM screen_layouts WHERE screen_id = $1`, [screenId] ); - - // ์ฐธ์กฐ ํ™”๋ฉด ์ถ”์ถœ - const referencedScreens = this.extractReferencedScreens( - layoutsResult.rows + referencedScreens.push( + ...this.extractReferencedScreens(layoutsResult.rows) ); + // V2 ๋ ˆ์ด์•„์›ƒ์—์„œ ์ฐธ์กฐ ํ™”๋ฉด ์ถ”์ถœ + const layoutsV2Result = await client.query<{ layout_data: any }>( + `SELECT layout_data FROM screen_layouts_v2 + WHERE screen_id = $1 AND company_code = $2`, + [screenId, sourceCompanyCode] + ); + for (const row of layoutsV2Result.rows) { + if (row.layout_data) { + this.extractScreenIdsFromObject(row.layout_data, referencedScreens); + } + } + if (referencedScreens.length > 0) { logger.info( ` ๐Ÿ“Ž ํ™”๋ฉด ${screenId}์—์„œ ์ฐธ์กฐ ํ™”๋ฉด ๋ฐœ๊ฒฌ: ${referencedScreens.join(", ")}` @@ -897,6 +926,8 @@ export class MenuCopyService { screenNameConfig?: { removeText?: string; addPrefix?: string; + replaceFrom?: string; + replaceTo?: string; }, additionalCopyOptions?: AdditionalCopyOptions ): Promise { @@ -939,7 +970,8 @@ export class MenuCopyService { const screenIds = await this.collectScreens( menus.map((m) => m.objid), sourceCompanyCode, - client + client, + menus ); const flowIds = await this.collectFlows(screenIds, client); @@ -1419,6 +1451,8 @@ export class MenuCopyService { screenNameConfig?: { removeText?: string; addPrefix?: string; + replaceFrom?: string; + replaceTo?: string; }, numberingRuleIdMap?: Map, menuIdMap?: Map @@ -1518,6 +1552,13 @@ export class MenuCopyService { // 3) ํ™”๋ฉด๋ช… ๋ณ€ํ™˜ ์ ์šฉ let transformedScreenName = screenDef.screen_name; if (screenNameConfig) { + if (screenNameConfig.replaceFrom?.trim()) { + transformedScreenName = transformedScreenName.replace( + new RegExp(screenNameConfig.replaceFrom.trim(), "g"), + screenNameConfig.replaceTo?.trim() || "" + ); + transformedScreenName = transformedScreenName.trim(); + } if (screenNameConfig.removeText?.trim()) { transformedScreenName = transformedScreenName.replace( new RegExp(screenNameConfig.removeText.trim(), "g"), @@ -2202,7 +2243,7 @@ export class MenuCopyService { menu.menu_url, menu.menu_desc, userId, - 'active', + menu.status || 'active', menu.system_name, targetCompanyCode, menu.lang_key, @@ -2332,8 +2373,9 @@ export class MenuCopyService { } /** - * ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ID ์žฌ๋งคํ•‘) + * ๋ฉ”๋‰ด URL + screen_code ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ID ์žฌ๋งคํ•‘) * menu_url์— ํฌํ•จ๋œ /screens/{screenId} ํ˜•์‹์˜ ํ™”๋ฉด ID๋ฅผ ๋ณต์ œ๋œ ํ™”๋ฉด ID๋กœ ๊ต์ฒด + * menu_info.screen_code๋„ ๋ณต์ œ๋œ screen_definitions.screen_code๋กœ ๊ต์ฒด */ private async updateMenuUrls( menuIdMap: Map, @@ -2341,56 +2383,121 @@ export class MenuCopyService { client: PoolClient ): Promise { if (menuIdMap.size === 0 || screenIdMap.size === 0) { - logger.info("๐Ÿ“ญ ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); + logger.info("๐Ÿ“ญ ๋ฉ”๋‰ด URL/screen_code ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); return; } const newMenuObjids = Array.from(menuIdMap.values()); - // ๋ณต์ œ๋œ ๋ฉ”๋‰ด ์ค‘ menu_url์ด ์žˆ๋Š” ๊ฒƒ ์กฐํšŒ - const menusWithUrl = await client.query<{ + // ๋ณต์ œ๋œ ๋ฉ”๋‰ด ์กฐํšŒ + const menusToUpdate = await client.query<{ objid: number; - menu_url: string; + menu_url: string | null; + screen_code: string | null; }>( - `SELECT objid, menu_url FROM menu_info - WHERE objid = ANY($1) AND menu_url IS NOT NULL AND menu_url != ''`, + `SELECT objid, menu_url, screen_code FROM menu_info + WHERE objid = ANY($1)`, [newMenuObjids] ); - if (menusWithUrl.rows.length === 0) { - logger.info("๐Ÿ“ญ menu_url ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); + if (menusToUpdate.rows.length === 0) { + logger.info("๐Ÿ“ญ ๋ฉ”๋‰ด URL/screen_code ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์—†์Œ"); return; } - let updatedCount = 0; - const screenIdPattern = /\/screens\/(\d+)/; - - for (const menu of menusWithUrl.rows) { - const match = menu.menu_url.match(screenIdPattern); - if (!match) continue; - - const originalScreenId = parseInt(match[1], 10); - const newScreenId = screenIdMap.get(originalScreenId); - - if (newScreenId && newScreenId !== originalScreenId) { - const newMenuUrl = menu.menu_url.replace( - `/screens/${originalScreenId}`, - `/screens/${newScreenId}` - ); - - await client.query( - `UPDATE menu_info SET menu_url = $1 WHERE objid = $2`, - [newMenuUrl, menu.objid] - ); - - logger.info( - ` ๐Ÿ”— ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ: ${menu.menu_url} โ†’ ${newMenuUrl}` - ); - updatedCount++; + // screenIdMap์˜ ์—ญ๋ฐฉํ–ฅ: ์›๋ณธ screen_id โ†’ ์ƒˆ screen_id์˜ screen_code ์กฐํšŒ + const newScreenIds = Array.from(screenIdMap.values()); + const screenCodeMap = new Map(); + if (newScreenIds.length > 0) { + const screenCodesResult = await client.query<{ + screen_id: number; + screen_code: string; + source_screen_id: number; + }>( + `SELECT sd_new.screen_id, sd_new.screen_code, sd_new.source_screen_id + FROM screen_definitions sd_new + WHERE sd_new.screen_id = ANY($1) AND sd_new.screen_code IS NOT NULL`, + [newScreenIds] + ); + for (const row of screenCodesResult.rows) { + if (row.source_screen_id) { + // ์›๋ณธ์˜ screen_code ์กฐํšŒ + const origResult = await client.query<{ screen_code: string }>( + `SELECT screen_code FROM screen_definitions WHERE screen_id = $1`, + [row.source_screen_id] + ); + if (origResult.rows[0]?.screen_code) { + screenCodeMap.set(origResult.rows[0].screen_code, row.screen_code); + } + } } } - logger.info(`โœ… ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${updatedCount}๊ฐœ`); + let updatedUrlCount = 0; + let updatedCodeCount = 0; + const screenIdPattern = /\/screens\/(\d+)/; + + for (const menu of menusToUpdate.rows) { + let newMenuUrl = menu.menu_url; + let newScreenCode = menu.screen_code; + let changed = false; + + // menu_url ์žฌ๋งคํ•‘ + if (menu.menu_url) { + const match = menu.menu_url.match(screenIdPattern); + if (match) { + const originalScreenId = parseInt(match[1], 10); + const newScreenId = screenIdMap.get(originalScreenId); + if (newScreenId && newScreenId !== originalScreenId) { + newMenuUrl = menu.menu_url.replace( + `/screens/${originalScreenId}`, + `/screens/${newScreenId}` + ); + changed = true; + updatedUrlCount++; + logger.info( + ` ๐Ÿ”— ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ: ${menu.menu_url} โ†’ ${newMenuUrl}` + ); + } + } + // /screen/{screen_code} ํ˜•์‹๋„ ์ฒ˜๋ฆฌ + const screenCodeUrlMatch = menu.menu_url.match(/\/screen\/(.+)/); + if (screenCodeUrlMatch && !menu.menu_url.match(/\/screens\//)) { + const origCode = screenCodeUrlMatch[1]; + const newCode = screenCodeMap.get(origCode); + if (newCode && newCode !== origCode) { + newMenuUrl = `/screen/${newCode}`; + changed = true; + updatedUrlCount++; + logger.info( + ` ๐Ÿ”— ๋ฉ”๋‰ด URL(์ฝ”๋“œ) ์—…๋ฐ์ดํŠธ: ${menu.menu_url} โ†’ ${newMenuUrl}` + ); + } + } + } + + // screen_code ์žฌ๋งคํ•‘ + if (menu.screen_code) { + const mappedCode = screenCodeMap.get(menu.screen_code); + if (mappedCode && mappedCode !== menu.screen_code) { + newScreenCode = mappedCode; + changed = true; + updatedCodeCount++; + logger.info( + ` ๐Ÿท๏ธ screen_code ์—…๋ฐ์ดํŠธ: ${menu.screen_code} โ†’ ${newScreenCode}` + ); + } + } + + if (changed) { + await client.query( + `UPDATE menu_info SET menu_url = $1, screen_code = $2 WHERE objid = $3`, + [newMenuUrl, newScreenCode, menu.objid] + ); + } + } + + logger.info(`โœ… ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ: ${updatedUrlCount}๊ฐœ, screen_code ์—…๋ฐ์ดํŠธ: ${updatedCodeCount}๊ฐœ`); } /** diff --git a/docker/dev/docker-compose.backend.mac.yml b/docker/dev/docker-compose.backend.mac.yml index 4d862d9e..ed4602dd 100644 --- a/docker/dev/docker-compose.backend.mac.yml +++ b/docker/dev/docker-compose.backend.mac.yml @@ -12,7 +12,7 @@ services: environment: - NODE_ENV=development - PORT=8080 - - DATABASE_URL=postgresql://postgres:ph0909!!@39.117.244.52:11132/plm + - DATABASE_URL=postgresql://postgres:vexplor0909!!@211.115.91.141:11134/vexplor - JWT_SECRET=ilshin-plm-super-secret-jwt-key-2024 - JWT_EXPIRES_IN=24h - CORS_ORIGIN=http://localhost:9771 diff --git a/scripts/menu-copy-automation.ts b/scripts/menu-copy-automation.ts new file mode 100644 index 00000000..af907d82 --- /dev/null +++ b/scripts/menu-copy-automation.ts @@ -0,0 +1,161 @@ +/** + * ๋ฉ”๋‰ด ๋ณต์‚ฌ ์ž๋™ํ™” ์Šคํฌ๋ฆฝํŠธ + * + * ์‹คํ–‰: npx ts-node scripts/menu-copy-automation.ts + * ๋˜๋Š”: npx playwright test scripts/menu-copy-automation.ts (playwright test ๋ชจ๋“œ) + * + * ์š”๊ตฌ์‚ฌํ•ญ: playwright ์„ค์น˜ (npm install playwright) + */ + +import { chromium, type Browser, type Page } from "playwright"; +import * as fs from "fs"; +import * as path from "path"; + +const BASE_URL = "http://localhost:9771"; +const SCREENSHOT_DIR = path.join(__dirname, "../screenshots-menu-copy"); + +// ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ +async function takeScreenshot(page: Page, stepName: string): Promise { + if (!fs.existsSync(SCREENSHOT_DIR)) { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + } + const filename = `${Date.now()}_${stepName}.png`; + const filepath = path.join(SCREENSHOT_DIR, filename); + await page.screenshot({ path: filepath, fullPage: true }); + console.log(`[์Šคํฌ๋ฆฐ์ƒท] ${stepName} -> ${filepath}`); + return filepath; +} + +async function main() { + let browser: Browser | null = null; + const screenshots: { step: string; path: string }[] = []; + + try { + console.log("=== ๋ฉ”๋‰ด ๋ณต์‚ฌ ์ž๋™ํ™” ์‹œ์ž‘ ===\n"); + + browser = await chromium.launch({ headless: false }); + const context = await browser.newContext({ + viewport: { width: 1280, height: 900 }, + ignoreHTTPSErrors: true, + }); + const page = await context.newPage(); + + // 1. ๋กœ๊ทธ์ธ + console.log("1. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ด๋™..."); + await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle" }); + await takeScreenshot(page, "01_login_page").then((p) => + screenshots.push({ step: "๋กœ๊ทธ์ธ ํŽ˜์ด์ง€", path: p }) + ); + + await page.fill('#userId', "admin"); + await page.fill('#password', "1234"); + await page.click('button[type="submit"]'); + await page.waitForTimeout(3000); + + await takeScreenshot(page, "02_after_login").then((p) => + screenshots.push({ step: "๋กœ๊ทธ์ธ ํ›„", path: p }) + ); + + // ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ wace ๊ณ„์ • ์‹œ๋„ (admin์ด DB์— ์—†์„ ์ˆ˜ ์žˆ์Œ) + const currentUrl = page.url(); + if (currentUrl.includes("/login")) { + console.log("admin ๋กœ๊ทธ์ธ ์‹คํŒจ, wace ๊ณ„์ •์œผ๋กœ ์žฌ์‹œ๋„..."); + await page.fill('#userId', "wace"); + await page.fill('#password', "1234"); + await page.click('button[type="submit"]'); + await page.waitForTimeout(3000); + } + + // 2. ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํŽ˜์ด์ง€๋กœ ์ด๋™ + console.log("2. ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํŽ˜์ด์ง€ ์ด๋™..."); + await page.goto(`${BASE_URL}/admin/menu`, { waitUntil: "networkidle" }); + await page.waitForTimeout(2000); + await takeScreenshot(page, "03_menu_page").then((p) => + screenshots.push({ step: "๋ฉ”๋‰ด ๊ด€๋ฆฌ ํŽ˜์ด์ง€", path: p }) + ); + + // 3. ํšŒ์‚ฌ ์„ ํƒ - ํƒ‘์”ฐ (COMPANY_7) + console.log("3. ํšŒ์‚ฌ ์„ ํƒ: ํƒ‘์”ฐ (COMPANY_7)..."); + const companyDropdown = page.locator('.company-dropdown button, button:has(svg)').first(); + await companyDropdown.click(); + await page.waitForTimeout(500); + + const topsealOption = page.getByText("ํƒ‘์”ฐ", { exact: false }).first(); + await topsealOption.click(); + await page.waitForTimeout(1500); + await takeScreenshot(page, "04_company_selected").then((p) => + screenshots.push({ step: "ํƒ‘์”ฐ ์„ ํƒ ํ›„", path: p }) + ); + + // 4. "์‚ฌ์šฉ์ž" ๋ฉ”๋‰ด ์ฐพ๊ธฐ ๋ฐ ๋ณต์‚ฌ ๋ฒ„ํŠผ ํด๋ฆญ + console.log("4. ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ์ฐพ๊ธฐ ๋ฐ ๋ณต์‚ฌ ๋ฒ„ํŠผ ํด๋ฆญ..."); + const userMenuRow = page.locator('tr').filter({ hasText: "์‚ฌ์šฉ์ž" }).first(); + await userMenuRow.waitFor({ timeout: 10000 }); + const copyButton = userMenuRow.getByRole("button", { name: "๋ณต์‚ฌ" }); + await copyButton.click(); + await page.waitForTimeout(1500); + await takeScreenshot(page, "05_copy_dialog_open").then((p) => + screenshots.push({ step: "๋ณต์‚ฌ ๋‹ค์ด์–ผ๋กœ๊ทธ", path: p }) + ); + + // 5. ๋Œ€์ƒ ํšŒ์‚ฌ ์„ ํƒ: ๋‘๋ฐ”์ด ๊ฐ•์ • ๋‹จ๋‹จ (COMPANY_18) + console.log("5. ๋Œ€์ƒ ํšŒ์‚ฌ ์„ ํƒ: ๋‘๋ฐ”์ด ๊ฐ•์ • ๋‹จ๋‹จ (COMPANY_18)..."); + const targetCompanyTrigger = page.locator('[id="company"]').or(page.getByRole("combobox")).first(); + await targetCompanyTrigger.click(); + await page.waitForTimeout(500); + + const dubaiOption = page.getByText("๋‘๋ฐ”์ด ๊ฐ•์ • ๋‹จ๋‹จ", { exact: false }).first(); + await dubaiOption.click(); + await page.waitForTimeout(500); + await takeScreenshot(page, "06_target_company_selected").then((p) => + screenshots.push({ step: "๋Œ€์ƒ ํšŒ์‚ฌ ์„ ํƒ ํ›„", path: p }) + ); + + // 6. ๋ณต์‚ฌ ์‹œ์ž‘ ๋ฒ„ํŠผ ํด๋ฆญ + console.log("6. ๋ณต์‚ฌ ์‹œ์ž‘..."); + const copyStartButton = page.getByRole("button", { name: /๋ณต์‚ฌ ์‹œ์ž‘|ํ™•์ธ/ }).first(); + await copyStartButton.click(); + + // 7. ๋ณต์‚ฌ ์™„๋ฃŒ ๋Œ€๊ธฐ (์ตœ๋Œ€ 5๋ถ„) + console.log("7. ๋ณต์‚ฌ ์™„๋ฃŒ ๋Œ€๊ธฐ (์ตœ๋Œ€ 5๋ถ„)..."); + try { + await page.waitForSelector('text=์™„๋ฃŒ, text=์„ฑ๊ณต, [role="status"]', { timeout: 300000 }); + await page.waitForTimeout(3000); + } catch { + console.log("ํƒ€์ž„์•„์›ƒ ๋˜๋Š” ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ๋Œ€๊ธฐ ์ค‘..."); + } + await takeScreenshot(page, "07_copy_result").then((p) => + screenshots.push({ step: "๋ณต์‚ฌ ๊ฒฐ๊ณผ", path: p }) + ); + + // ๊ฒฐ๊ณผ ํ™•์ธ + const resultText = await page.locator("body").textContent(); + if (resultText?.includes("์™„๋ฃŒ") || resultText?.includes("์„ฑ๊ณต")) { + console.log("\n=== ๋ฉ”๋‰ด ๋ณต์‚ฌ ์„ฑ๊ณต ==="); + } else if (resultText?.includes("์˜ค๋ฅ˜") || resultText?.includes("์‹คํŒจ") || resultText?.includes("error")) { + console.log("\n=== ์—๋Ÿฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ - ์Šคํฌ๋ฆฐ์ƒท ํ™•์ธ ํ•„์š” ==="); + } + + console.log("\n=== ์Šคํฌ๋ฆฐ์ƒท ๋ชฉ๋ก ==="); + screenshots.forEach((s) => console.log(` - ${s.step}: ${s.path}`)); + } catch (error) { + console.error("์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); + if (browser) { + const pages = (browser as any).contexts?.()?.[0]?.pages?.() || []; + for (const p of pages) { + try { + await takeScreenshot(p, "error_state").then((path) => + screenshots.push({ step: "์—๋Ÿฌ ์ƒํƒœ", path }) + ); + } catch {} + } + } + throw error; + } finally { + if (browser) { + await browser.close(); + } + } +} + +main().catch(console.error); From 4f639dec345302633f05dd935bc0e38a9e8dc755 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 5 Mar 2026 10:09:37 +0900 Subject: [PATCH 2/6] feat: Implement screen group screens duplication in menu copy service - Added a new method `copyScreenGroupScreens` to handle the duplication of screen group screens during the menu copy process. - Implemented logic to create a mapping of screen group IDs from the source to the target company. - Enhanced the existing menu copy functionality to include the copying of screen group screens, ensuring that the screen-role and display order are preserved. - Added logging for better traceability of the duplication process. This update improves the menu copy service by allowing for a more comprehensive duplication of associated screen group screens, enhancing the overall functionality of the menu management system. --- backend-node/src/services/menuCopyService.ts | 108 ++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index 747d5427..f67e09a3 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -1127,6 +1127,16 @@ export class MenuCopyService { logger.info("\n๐Ÿ”„ [6.5๋‹จ๊ณ„] ๋ฉ”๋‰ด URL ํ™”๋ฉด ID ์žฌ๋งคํ•‘"); await this.updateMenuUrls(menuIdMap, screenIdMap, client); + // === 6.7๋‹จ๊ณ„: screen_group_screens ๋ณต์ œ === + logger.info("\n๐Ÿท๏ธ [6.7๋‹จ๊ณ„] screen_group_screens ๋ณต์ œ"); + await this.copyScreenGroupScreens( + screenIds, + screenIdMap, + sourceCompanyCode, + targetCompanyCode, + client + ); + // === 7๋‹จ๊ณ„: ํ…Œ์ด๋ธ” ํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ === if (additionalCopyOptions?.copyTableTypeColumns) { logger.info("\n๐Ÿ“ฆ [7๋‹จ๊ณ„] ํ…Œ์ด๋ธ” ํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ"); @@ -2108,6 +2118,26 @@ export class MenuCopyService { logger.info(`๐Ÿ“‚ ๋ฉ”๋‰ด ๋ณต์‚ฌ ์ค‘: ${menus.length}๊ฐœ`); + // screen_group_id ์žฌ๋งคํ•‘ ๋งต ์ƒ์„ฑ (source company โ†’ target company) + const screenGroupIdMap = new Map(); + const sourceGroupIds = [...new Set(menus.map(m => m.screen_group_id).filter(Boolean))] as number[]; + if (sourceGroupIds.length > 0) { + const sourceGroups = await client.query<{ id: number; group_name: string }>( + `SELECT id, group_name FROM screen_groups WHERE id = ANY($1)`, + [sourceGroupIds] + ); + for (const sg of sourceGroups.rows) { + const targetGroup = await client.query<{ id: number }>( + `SELECT id FROM screen_groups WHERE group_name = $1 AND company_code = $2 LIMIT 1`, + [sg.group_name, targetCompanyCode] + ); + if (targetGroup.rows.length > 0) { + screenGroupIdMap.set(sg.id, targetGroup.rows[0].id); + } + } + logger.info(`๐Ÿท๏ธ screen_group ๋งคํ•‘: ${screenGroupIdMap.size}/${sourceGroupIds.length}๊ฐœ`); + } + // ์œ„์ƒ ์ •๋ ฌ (๋ถ€๋ชจ ๋จผ์ € ์‚ฝ์ž…) const sortedMenus = this.topologicalSortMenus(menus); @@ -2252,7 +2282,7 @@ export class MenuCopyService { menu.menu_code, sourceMenuObjid, menu.menu_icon, - menu.screen_group_id, + menu.screen_group_id ? (screenGroupIdMap.get(menu.screen_group_id) || menu.screen_group_id) : null, ] ); @@ -2500,6 +2530,82 @@ export class MenuCopyService { logger.info(`โœ… ๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ: ${updatedUrlCount}๊ฐœ, screen_code ์—…๋ฐ์ดํŠธ: ${updatedCodeCount}๊ฐœ`); } + /** + * screen_group_screens ๋ณต์ œ (ํ™”๋ฉด-์Šคํฌ๋ฆฐ๊ทธ๋ฃน ๋งคํ•‘) + */ + private async copyScreenGroupScreens( + screenIds: Set, + screenIdMap: Map, + sourceCompanyCode: string, + targetCompanyCode: string, + client: PoolClient + ): Promise { + if (screenIds.size === 0 || screenIdMap.size === 0) { + logger.info("๐Ÿ“ญ screen_group_screens ๋ณต์ œ ๋Œ€์ƒ ์—†์Œ"); + return; + } + + // ๊ธฐ์กด COMPANY_10์˜ screen_group_screens ์‚ญ์ œ (๊นจ์ง„ ์ด์ „ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ) + await client.query( + `DELETE FROM screen_group_screens WHERE company_code = $1`, + [targetCompanyCode] + ); + + // ์†Œ์Šค ํšŒ์‚ฌ์˜ screen_group_screens ์กฐํšŒ + const sourceScreenIds = Array.from(screenIds); + const sourceResult = await client.query<{ + group_id: number; + screen_id: number; + screen_role: string; + display_order: number; + is_default: string; + }>( + `SELECT group_id, screen_id, screen_role, display_order, is_default + FROM screen_group_screens + WHERE company_code = $1 AND screen_id = ANY($2)`, + [sourceCompanyCode, sourceScreenIds] + ); + + if (sourceResult.rows.length === 0) { + logger.info("๐Ÿ“ญ ์†Œ์Šค์— screen_group_screens ์—†์Œ"); + return; + } + + // screen_group ID ๋งคํ•‘ (source group_name โ†’ target group_id) + const sourceGroupIds = [...new Set(sourceResult.rows.map(r => r.group_id))]; + const sourceGroups = await client.query<{ id: number; group_name: string }>( + `SELECT id, group_name FROM screen_groups WHERE id = ANY($1)`, + [sourceGroupIds] + ); + const groupIdMap = new Map(); + for (const sg of sourceGroups.rows) { + const targetGroup = await client.query<{ id: number }>( + `SELECT id FROM screen_groups WHERE group_name = $1 AND company_code = $2 LIMIT 1`, + [sg.group_name, targetCompanyCode] + ); + if (targetGroup.rows.length > 0) { + groupIdMap.set(sg.id, targetGroup.rows[0].id); + } + } + + let insertedCount = 0; + for (const row of sourceResult.rows) { + const newGroupId = groupIdMap.get(row.group_id); + const newScreenId = screenIdMap.get(row.screen_id); + if (!newGroupId || !newScreenId) continue; + + await client.query( + `INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer) + VALUES ($1, $2, $3, $4, $5, $6, 'system') + ON CONFLICT DO NOTHING`, + [newGroupId, newScreenId, row.screen_role, row.display_order, row.is_default, targetCompanyCode] + ); + insertedCount++; + } + + logger.info(`โœ… screen_group_screens ๋ณต์ œ: ${insertedCount}๊ฐœ`); + } + /** * ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์‚ฌ (์ตœ์ ํ™”: ๋ฐฐ์น˜ ์กฐํšŒ/์‚ฝ์ž…) */ From 4b8f2b783966b25e603e0df682eb3b3ea5abd7a1 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 5 Mar 2026 11:30:31 +0900 Subject: [PATCH 3/6] feat: Update screen reference handling in V2 layouts - Enhanced the `ScreenManagementService` to include updates for V2 layouts in the `screen_layouts_v2` table. - Implemented logic to remap `screenId`, `targetScreenId`, `modalScreenId`, and other related IDs in layout data. - Added logging for the number of layouts updated in both V1 and V2, improving traceability of the update process. - This update ensures that screen references are correctly maintained across different layout versions, enhancing the overall functionality of the screen management system. --- .../src/services/screenManagementService.ts | 123 ++- .../screen/config-panels/button/ActionTab.tsx | 17 + .../screen/config-panels/button/BasicTab.tsx | 40 + .../screen/config-panels/button/DataTab.tsx | 872 ++++++++++++++++++ .../components/common/ConfigField.tsx | 264 ++++++ .../components/common/ConfigPanelBuilder.tsx | 76 ++ .../components/common/ConfigPanelTypes.ts | 56 ++ .../components/common/ConfigSection.tsx | 52 ++ .../config-panels/CommonConfigTab.tsx | 90 ++ .../config-panels/LeftPanelConfigTab.tsx | 606 ++++++++++++ .../config-panels/RightPanelConfigTab.tsx | 801 ++++++++++++++++ .../config-panels/SharedComponents.tsx | 256 +++++ .../config-panels/BasicConfigPanel.tsx | 125 +++ .../config-panels/ColumnsConfigPanel.tsx | 534 +++++++++++ .../config-panels/OptionsConfigPanel.tsx | 290 ++++++ .../config-panels/StyleConfigPanel.tsx | 129 +++ 16 files changed, 4328 insertions(+), 3 deletions(-) create mode 100644 frontend/components/screen/config-panels/button/ActionTab.tsx create mode 100644 frontend/components/screen/config-panels/button/BasicTab.tsx create mode 100644 frontend/components/screen/config-panels/button/DataTab.tsx create mode 100644 frontend/lib/registry/components/common/ConfigField.tsx create mode 100644 frontend/lib/registry/components/common/ConfigPanelBuilder.tsx create mode 100644 frontend/lib/registry/components/common/ConfigPanelTypes.ts create mode 100644 frontend/lib/registry/components/common/ConfigSection.tsx create mode 100644 frontend/lib/registry/components/v2-split-panel-layout/config-panels/CommonConfigTab.tsx create mode 100644 frontend/lib/registry/components/v2-split-panel-layout/config-panels/LeftPanelConfigTab.tsx create mode 100644 frontend/lib/registry/components/v2-split-panel-layout/config-panels/RightPanelConfigTab.tsx create mode 100644 frontend/lib/registry/components/v2-split-panel-layout/config-panels/SharedComponents.tsx create mode 100644 frontend/lib/registry/components/v2-table-list/config-panels/BasicConfigPanel.tsx create mode 100644 frontend/lib/registry/components/v2-table-list/config-panels/ColumnsConfigPanel.tsx create mode 100644 frontend/lib/registry/components/v2-table-list/config-panels/OptionsConfigPanel.tsx create mode 100644 frontend/lib/registry/components/v2-table-list/config-panels/StyleConfigPanel.tsx diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index a75fc431..4c5bdc57 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -3482,8 +3482,74 @@ export class ScreenManagementService { } console.log( - `โœ… screenId/modalScreenId/targetScreenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${result.updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ`, + `โœ… V1 screenId/modalScreenId/targetScreenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${result.updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ`, ); + + // V2 ๋ ˆ์ด์•„์›ƒ(screen_layouts_v2)๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ + const v2LayoutsResult = await client.query( + `SELECT screen_id, layer_id, company_code, layout_data + FROM screen_layouts_v2 + WHERE screen_id IN (${placeholders}) + AND layout_data::text ~ '"(screenId|targetScreenId|modalScreenId|leftScreenId|rightScreenId|addModalScreenId|editModalScreenId)"'`, + targetScreenIds, + ); + + console.log( + `๐Ÿ” V2 ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ๋ ˆ์ด์•„์›ƒ: ${v2LayoutsResult.rows.length}๊ฐœ`, + ); + + let v2Updated = 0; + for (const v2Layout of v2LayoutsResult.rows) { + let layoutData = v2Layout.layout_data; + if (!layoutData) continue; + + let v2HasChanges = false; + + const updateV2References = (obj: any): void => { + if (!obj || typeof obj !== "object") return; + if (Array.isArray(obj)) { + for (const item of obj) updateV2References(item); + return; + } + for (const key of Object.keys(obj)) { + const value = obj[key]; + if ( + (key === "screenId" || key === "targetScreenId" || key === "modalScreenId" || + key === "leftScreenId" || key === "rightScreenId" || + key === "addModalScreenId" || key === "editModalScreenId") + ) { + const numVal = typeof value === "number" ? value : parseInt(value); + if (!isNaN(numVal) && numVal > 0) { + const newId = screenMap.get(numVal); + if (newId) { + obj[key] = typeof value === "number" ? newId : String(newId); + v2HasChanges = true; + console.log(`๐Ÿ”— V2 ${key} ๋งคํ•‘: ${numVal} โ†’ ${newId}`); + } + } + } + if (typeof value === "object" && value !== null) { + updateV2References(value); + } + } + }; + + updateV2References(layoutData); + + if (v2HasChanges) { + await client.query( + `UPDATE screen_layouts_v2 SET layout_data = $1, updated_at = NOW() + WHERE screen_id = $2 AND layer_id = $3 AND company_code = $4`, + [JSON.stringify(layoutData), v2Layout.screen_id, v2Layout.layer_id, v2Layout.company_code], + ); + v2Updated++; + } + } + + console.log( + `โœ… V2 ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${v2Updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ`, + ); + result.updated += v2Updated; }); return result; @@ -4610,9 +4676,60 @@ export class ScreenManagementService { } console.log( - `โœ… ์ด ${updateCount}๊ฐœ ๋ ˆ์ด์•„์›ƒ์˜ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด ID ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ (๋ฒ„ํŠผ + ์กฐ๊ฑด๋ถ€์ปจํ…Œ์ด๋„ˆ)`, + `โœ… V1: ${updateCount}๊ฐœ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`, ); - return updateCount; + + // V2 ๋ ˆ์ด์•„์›ƒ(screen_layouts_v2)์—์„œ๋„ targetScreenId ๋“ฑ ์žฌ๋งคํ•‘ + const v2Layouts = await query( + `SELECT screen_id, layer_id, company_code, layout_data + FROM screen_layouts_v2 + WHERE screen_id = $1 + AND layout_data IS NOT NULL`, + [screenId], + ); + + let v2UpdateCount = 0; + for (const v2Layout of v2Layouts) { + const layoutData = v2Layout.layout_data; + if (!layoutData?.components) continue; + + let v2Changed = false; + const updateV2Refs = (obj: any): void => { + if (!obj || typeof obj !== "object") return; + if (Array.isArray(obj)) { for (const item of obj) updateV2Refs(item); return; } + for (const key of Object.keys(obj)) { + const value = obj[key]; + if ( + (key === "targetScreenId" || key === "screenId" || key === "modalScreenId" || + key === "leftScreenId" || key === "rightScreenId" || + key === "addModalScreenId" || key === "editModalScreenId") + ) { + const numVal = typeof value === "number" ? value : parseInt(value); + if (!isNaN(numVal) && screenIdMapping.has(numVal)) { + obj[key] = typeof value === "number" ? screenIdMapping.get(numVal)! : screenIdMapping.get(numVal)!.toString(); + v2Changed = true; + } + } + if (typeof value === "object" && value !== null) updateV2Refs(value); + } + }; + updateV2Refs(layoutData); + + if (v2Changed) { + await query( + `UPDATE screen_layouts_v2 SET layout_data = $1, updated_at = NOW() + WHERE screen_id = $2 AND layer_id = $3 AND company_code = $4`, + [JSON.stringify(layoutData), v2Layout.screen_id, v2Layout.layer_id, v2Layout.company_code], + ); + v2UpdateCount++; + } + } + + const total = updateCount + v2UpdateCount; + console.log( + `โœ… ์ด ${total}๊ฐœ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ (V1: ${updateCount}, V2: ${v2UpdateCount})`, + ); + return total; } /** diff --git a/frontend/components/screen/config-panels/button/ActionTab.tsx b/frontend/components/screen/config-panels/button/ActionTab.tsx new file mode 100644 index 00000000..f6c872e6 --- /dev/null +++ b/frontend/components/screen/config-panels/button/ActionTab.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; + +export interface ActionTabProps { + config: any; + onChange: (key: string, value: any) => void; + children: React.ReactNode; +} + +/** + * ๋™์ž‘ ํƒญ: ํด๋ฆญ ์ด๋ฒคํŠธ, ๋„ค๋น„๊ฒŒ์ด์…˜, ๋ชจ๋‹ฌ ์—ด๊ธฐ, ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋“ฑ ๋™์ž‘ ์„ค์ • + * ์‹ค์ œ UI๋Š” ๋ฉ”์ธ ButtonConfigPanel์—์„œ ๋ Œ๋”๋ง ํ›„ children์œผ๋กœ ์ „๋‹ฌ + */ +export const ActionTab: React.FC = ({ children }) => { + return
{children}
; +}; diff --git a/frontend/components/screen/config-panels/button/BasicTab.tsx b/frontend/components/screen/config-panels/button/BasicTab.tsx new file mode 100644 index 00000000..1eb7d2f7 --- /dev/null +++ b/frontend/components/screen/config-panels/button/BasicTab.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; + +export interface BasicTabProps { + config: any; + onChange: (key: string, value: any) => void; + localText?: string; + onTextChange?: (value: string) => void; +} + +export const BasicTab: React.FC = ({ + config, + onChange, + localText, + onTextChange, +}) => { + const text = localText !== undefined ? localText : (config.text !== undefined ? config.text : "๋ฒ„ํŠผ"); + + const handleChange = (newValue: string) => { + onTextChange?.(newValue); + onChange("componentConfig.text", newValue); + }; + + return ( +
+
+ + handleChange(e.target.value)} + placeholder="๋ฒ„ํŠผ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + /> +
+
+ ); +}; diff --git a/frontend/components/screen/config-panels/button/DataTab.tsx b/frontend/components/screen/config-panels/button/DataTab.tsx new file mode 100644 index 00000000..29b35c78 --- /dev/null +++ b/frontend/components/screen/config-panels/button/DataTab.tsx @@ -0,0 +1,872 @@ +"use client"; + +import React from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Button } from "@/components/ui/button"; +import { Check, ChevronsUpDown, Plus, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { QuickInsertConfigSection } from "../QuickInsertConfigSection"; +import { ComponentData } from "@/types/screen"; + +export interface DataTabProps { + config: any; + onChange: (key: string, value: any) => void; + component: ComponentData; + allComponents: ComponentData[]; + currentTableName?: string; + availableTables: Array<{ name: string; label: string }>; + mappingTargetColumns: Array<{ name: string; label: string }>; + mappingSourceColumnsMap: Record>; + currentTableColumns: Array<{ name: string; label: string }>; + mappingSourcePopoverOpen: Record; + setMappingSourcePopoverOpen: React.Dispatch>>; + mappingTargetPopoverOpen: Record; + setMappingTargetPopoverOpen: React.Dispatch>>; + activeMappingGroupIndex: number; + setActiveMappingGroupIndex: React.Dispatch>; + loadMappingColumns: (tableName: string) => Promise>; + setMappingSourceColumnsMap: React.Dispatch< + React.SetStateAction>> + >; +} + +export const DataTab: React.FC = ({ + config, + onChange, + component, + allComponents, + currentTableName, + availableTables, + mappingTargetColumns, + mappingSourceColumnsMap, + currentTableColumns, + mappingSourcePopoverOpen, + setMappingSourcePopoverOpen, + mappingTargetPopoverOpen, + setMappingTargetPopoverOpen, + activeMappingGroupIndex, + setActiveMappingGroupIndex, + loadMappingColumns, + setMappingSourceColumnsMap, +}) => { + const actionType = config.action?.type; + const onUpdateProperty = (path: string, value: any) => onChange(path, value); + + if (actionType === "quickInsert") { + return ( +
+ +
+ ); + } + + if (actionType !== "transferData") { + return ( +
+ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋˜๋Š” ์ฆ‰์‹œ ์ €์žฅ ์•ก์…˜์„ ์„ ํƒํ•˜๋ฉด ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +
+ ); + } + + return ( +
+
+

๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์„ค์ •

+ +
+ + +

+ ๋ ˆ์ด์–ด๋ณ„๋กœ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์ด ์žˆ์„ ๊ฒฝ์šฐ "์ž๋™ ํƒ์ƒ‰"์„ ์„ ํƒํ•˜๋ฉด ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋ ˆ์ด์–ด์˜ ํ…Œ์ด๋ธ”์„ ์ž๋™์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค +

+
+ +
+ + + {config.action?.dataTransfer?.targetType === "splitPanel" && ( +

+ ์ด ๋ฒ„ํŠผ์ด ๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ขŒ์ธก ํ™”๋ฉด์—์„œ ์šฐ์ธก์œผ๋กœ, ๋˜๋Š” ์šฐ์ธก์—์„œ ์ขŒ์ธก์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. +

+ )} +
+ + {config.action?.dataTransfer?.targetType === "component" && ( +
+ + +

ํ…Œ์ด๋ธ”, ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน ๋“ฑ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๋Š” ์ปดํฌ๋„ŒํŠธ

+
+ )} + + {config.action?.dataTransfer?.targetType === "splitPanel" && ( +
+ + + onUpdateProperty("componentConfig.action.dataTransfer.targetComponentId", e.target.value) + } + placeholder="๋น„์›Œ๋‘๋ฉด ์ฒซ ๋ฒˆ์งธ ์ˆ˜์‹  ๊ฐ€๋Šฅ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ" + className="h-8 text-xs" + /> +

+ ๋ฐ˜๋Œ€ํŽธ ํ™”๋ฉด์˜ ํŠน์ • ์ปดํฌ๋„ŒํŠธ ID๋ฅผ ์ง€์ •ํ•˜๊ฑฐ๋‚˜, ๋น„์›Œ๋‘๋ฉด ์ž๋™์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ์ˆ˜์‹  ๊ฐ€๋Šฅ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. +

+
+ )} + +
+ + +

๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์„ ํƒ

+
+ +
+
+ +

๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํ›„ ์†Œ์Šค์˜ ์„ ํƒ์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค

+
+ + onUpdateProperty("componentConfig.action.dataTransfer.clearAfterTransfer", checked) + } + /> +
+ +
+
+ +

๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ „ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค

+
+ + onUpdateProperty("componentConfig.action.dataTransfer.confirmBeforeTransfer", checked) + } + /> +
+ + {config.action?.dataTransfer?.confirmBeforeTransfer && ( +
+ + onUpdateProperty("componentConfig.action.dataTransfer.confirmMessage", e.target.value)} + className="h-8 text-xs" + /> +
+ )} + +
+ +
+
+ + + onUpdateProperty( + "componentConfig.action.dataTransfer.validation.minSelection", + parseInt(e.target.value) || 0, + ) + } + className="h-8 w-20 text-xs" + /> +
+
+ + + onUpdateProperty( + "componentConfig.action.dataTransfer.validation.maxSelection", + parseInt(e.target.value) || undefined, + ) + } + className="h-8 w-20 text-xs" + /> +
+
+
+ +
+ +

+ ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋“ฑ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค +

+
+
+ + +

+ ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ, ์…€๋ ‰ํŠธ๋ฐ•์Šค ๋“ฑ (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ „๋‹ฌ์šฉ) +

+
+
+ + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + { + const currentSources = config.action?.dataTransfer?.additionalSources || []; + const newSources = [...currentSources]; + if (newSources.length === 0) { + newSources.push({ componentId: "", fieldName: "" }); + } else { + newSources[0] = { ...newSources[0], fieldName: "" }; + } + onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); + }} + className="text-xs" + > + + ์„ ํƒ ์•ˆ ํ•จ (์ „์ฒด ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ) + + {(mappingTargetColumns.length > 0 ? mappingTargetColumns : currentTableColumns).map((col) => ( + { + const currentSources = config.action?.dataTransfer?.additionalSources || []; + const newSources = [...currentSources]; + if (newSources.length === 0) { + newSources.push({ componentId: "", fieldName: col.name }); + } else { + newSources[0] = { ...newSources[0], fieldName: col.name }; + } + onUpdateProperty("componentConfig.action.dataTransfer.additionalSources", newSources); + }} + className="text-xs" + > + + {col.label || col.name} + {col.label && col.label !== col.name && ( + ({col.name}) + )} + + ))} + + + + + +

์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋  ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ

+
+
+
+ +
+ +
+ + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {availableTables.map((table) => ( + { + onUpdateProperty("componentConfig.action.dataTransfer.targetTable", table.name); + }} + className="text-xs" + > + + {table.label} + ({table.name}) + + ))} + + + + + +
+ +
+
+ + +
+

+ ์—ฌ๋Ÿฌ ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ, ๊ฐ ํ…Œ์ด๋ธ”๋ณ„๋กœ ๋งคํ•‘ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Ÿฐํƒ€์ž„์— ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ž๋™ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. +

+ + {!config.action?.dataTransfer?.targetTable ? ( +
+

๋จผ์ € ํƒ€๊ฒŸ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”.

+
+ ) : !(config.action?.dataTransfer?.multiTableMappings || []).length ? ( +
+

๋งคํ•‘ ๊ทธ๋ฃน์ด ์—†์Šต๋‹ˆ๋‹ค. ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

+
+ ) : ( +
+
+ {(config.action?.dataTransfer?.multiTableMappings || []).map((group: any, gIdx: number) => ( +
+ + +
+ ))} +
+ + {(() => { + const multiMappings = config.action?.dataTransfer?.multiTableMappings || []; + const activeGroup = multiMappings[activeMappingGroupIndex]; + if (!activeGroup) return null; + + const activeSourceTable = activeGroup.sourceTable || ""; + const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || []; + const activeRules: any[] = activeGroup.mappingRules || []; + + const updateGroupField = (field: string, value: any) => { + const mappings = [...multiMappings]; + mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value }; + onUpdateProperty("componentConfig.action.dataTransfer.multiTableMappings", mappings); + }; + + return ( +
+
+ + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + + {availableTables.map((table) => ( + { + updateGroupField("sourceTable", table.name); + if (!mappingSourceColumnsMap[table.name]) { + const cols = await loadMappingColumns(table.name); + setMappingSourceColumnsMap((prev) => ({ ...prev, [table.name]: cols })); + } + }} + className="text-xs" + > + + {table.label} + ({table.name}) + + ))} + + + + + +
+ +
+
+ + +
+ + {!activeSourceTable ? ( +

์†Œ์Šค ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์„ ํƒํ•˜์„ธ์š”.

+ ) : activeRules.length === 0 ? ( +

๋งคํ•‘ ์—†์Œ (๋™์ผ ํ•„๋“œ๋ช… ์ž๋™ ๋งคํ•‘)

+ ) : ( + activeRules.map((rule: any, rIdx: number) => { + const popoverKeyS = `${activeMappingGroupIndex}-${rIdx}-s`; + const popoverKeyT = `${activeMappingGroupIndex}-${rIdx}-t`; + return ( +
+
+ + setMappingSourcePopoverOpen((prev) => ({ ...prev, [popoverKeyS]: open })) + } + > + + + + + + + + ์ปฌ๋Ÿผ ์—†์Œ + + {activeSourceColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], sourceField: col.name }; + updateGroupField("mappingRules", newRules); + setMappingSourcePopoverOpen((prev) => ({ + ...prev, + [popoverKeyS]: false, + })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ( + ({col.name}) + )} + + ))} + + + + + +
+ + โ†’ + +
+ + setMappingTargetPopoverOpen((prev) => ({ ...prev, [popoverKeyT]: open })) + } + > + + + + + + + + ์ปฌ๋Ÿผ ์—†์Œ + + {mappingTargetColumns.map((col) => ( + { + const newRules = [...activeRules]; + newRules[rIdx] = { ...newRules[rIdx], targetField: col.name }; + updateGroupField("mappingRules", newRules); + setMappingTargetPopoverOpen((prev) => ({ + ...prev, + [popoverKeyT]: false, + })); + }} + className="text-xs" + > + + {col.label} + {col.label !== col.name && ( + ({col.name}) + )} + + ))} + + + + + +
+ + +
+ ); + }) + )} +
+
+ ); + })()} +
+ )} +
+
+ +
+

+ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•: +
+ 1. ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค +
+ 2. ์†Œ์Šค ํ…Œ์ด๋ธ”๋ณ„๋กœ ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค +
+ 3. ์ด ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ”์„ ์ž๋™ ๊ฐ์ง€ํ•˜์—ฌ ๋งคํ•‘๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ํƒ€๊ฒŸ์œผ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค +

+
+
+
+ ); +}; diff --git a/frontend/lib/registry/components/common/ConfigField.tsx b/frontend/lib/registry/components/common/ConfigField.tsx new file mode 100644 index 00000000..0b11780d --- /dev/null +++ b/frontend/lib/registry/components/common/ConfigField.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React from "react"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Plus, X } from "lucide-react"; +import { ConfigFieldDefinition, ConfigOption } from "./ConfigPanelTypes"; + +interface ConfigFieldProps { + field: ConfigFieldDefinition; + value: any; + onChange: (key: string, value: any) => void; + tableColumns?: ConfigOption[]; +} + +export function ConfigField({ + field, + value, + onChange, + tableColumns, +}: ConfigFieldProps) { + const handleChange = (newValue: any) => { + onChange(field.key, newValue); + }; + + const renderField = () => { + switch (field.type) { + case "text": + return ( + handleChange(e.target.value)} + placeholder={field.placeholder} + className="h-8 text-xs" + /> + ); + + case "number": + return ( + + handleChange( + e.target.value === "" ? undefined : Number(e.target.value), + ) + } + placeholder={field.placeholder} + min={field.min} + max={field.max} + step={field.step} + className="h-8 text-xs" + /> + ); + + case "switch": + return ( + + ); + + case "select": + return ( + + ); + + case "textarea": + return ( +