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);