Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node
This commit is contained in:
parent
b9080d03f6
commit
772514c270
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,8 @@ export class MenuCopyService {
|
|||
private async collectScreens(
|
||||
menuObjids: number[],
|
||||
sourceCompanyCode: string,
|
||||
client: PoolClient
|
||||
client: PoolClient,
|
||||
menus?: Menu[]
|
||||
): Promise<Set<number>> {
|
||||
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<ScreenLayout>(
|
||||
`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<MenuCopyResult> {
|
||||
|
|
@ -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<string, string>,
|
||||
menuIdMap?: Map<number, number>
|
||||
|
|
@ -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<number, number>,
|
||||
|
|
@ -2341,56 +2383,121 @@ export class MenuCopyService {
|
|||
client: PoolClient
|
||||
): Promise<void> {
|
||||
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<string, string>();
|
||||
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}개`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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);
|
||||
Loading…
Reference in New Issue