Merge remote-tracking branch 'upstream/main'
Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s
Some checks failed
Build and Push Images / build-and-push (push) Failing after 49s
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Phase 2-1B: 핵심 인증 API 구현
|
||||
|
||||
import { Router } from "express";
|
||||
import { checkAuthStatus } from "../middleware/authMiddleware";
|
||||
import { AuthController } from "../controllers/authController";
|
||||
|
||||
const router = Router();
|
||||
@@ -12,7 +11,7 @@ const router = Router();
|
||||
* 인증 상태 확인 API
|
||||
* 기존 Java ApiLoginController.checkAuthStatus() 포팅
|
||||
*/
|
||||
router.get("/status", checkAuthStatus);
|
||||
router.get("/status", AuthController.checkAuthStatus);
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
|
||||
@@ -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);
|
||||
@@ -1095,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단계] 테이블 타입 설정 복사");
|
||||
@@ -1419,6 +1461,8 @@ export class MenuCopyService {
|
||||
screenNameConfig?: {
|
||||
removeText?: string;
|
||||
addPrefix?: string;
|
||||
replaceFrom?: string;
|
||||
replaceTo?: string;
|
||||
},
|
||||
numberingRuleIdMap?: Map<string, string>,
|
||||
menuIdMap?: Map<number, number>
|
||||
@@ -1518,6 +1562,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"),
|
||||
@@ -1535,20 +1586,21 @@ export class MenuCopyService {
|
||||
// === 기존 복사본이 있는 경우: 업데이트 ===
|
||||
const existingScreenId = existingCopy.screen_id;
|
||||
|
||||
// 원본 V2 레이아웃 조회
|
||||
// 원본 V2 레이아웃 조회 (모든 레이어)
|
||||
const sourceLayoutV2Result = await client.query<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`,
|
||||
`SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1 ORDER BY layer_id`,
|
||||
[originalScreenId]
|
||||
);
|
||||
|
||||
// 대상 V2 레이아웃 조회
|
||||
// 대상 V2 레이아웃 조회 (모든 레이어)
|
||||
const targetLayoutV2Result = await client.query<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`,
|
||||
`SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1 ORDER BY layer_id`,
|
||||
[existingScreenId]
|
||||
);
|
||||
|
||||
// 변경 여부 확인 (V2 레이아웃 비교)
|
||||
const hasChanges = this.hasLayoutChangesV2(
|
||||
// 변경 여부 확인: 레이어 수가 다르면 무조건 변경됨
|
||||
const layerCountDiffers = sourceLayoutV2Result.rows.length !== targetLayoutV2Result.rows.length;
|
||||
const hasChanges = layerCountDiffers || this.hasLayoutChangesV2(
|
||||
sourceLayoutV2Result.rows[0]?.layout_data,
|
||||
targetLayoutV2Result.rows[0]?.layout_data
|
||||
);
|
||||
@@ -1652,7 +1704,7 @@ export class MenuCopyService {
|
||||
}
|
||||
}
|
||||
|
||||
// === 2단계: screen_layouts_v2 처리 (이제 screenIdMap이 완성됨) ===
|
||||
// === 2단계: screen_conditional_zones + screen_layouts_v2 처리 (멀티 레이어 지원) ===
|
||||
logger.info(
|
||||
`\n📐 V2 레이아웃 처리 시작 (screenIdMap 완성: ${screenIdMap.size}개)`
|
||||
);
|
||||
@@ -1664,23 +1716,90 @@ export class MenuCopyService {
|
||||
isUpdate,
|
||||
} of screenDefsToProcess) {
|
||||
try {
|
||||
// 원본 V2 레이아웃 조회
|
||||
const layoutV2Result = await client.query<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2 WHERE screen_id = $1`,
|
||||
const sourceCompanyCode = screenDef.company_code;
|
||||
|
||||
// 원본 V2 레이아웃 전체 조회 (모든 레이어)
|
||||
const layoutV2Result = await client.query<{
|
||||
layout_data: any;
|
||||
layer_id: number;
|
||||
layer_name: string;
|
||||
condition_config: any;
|
||||
}>(
|
||||
`SELECT layout_data, layer_id, layer_name, condition_config
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id`,
|
||||
[originalScreenId, sourceCompanyCode]
|
||||
);
|
||||
|
||||
if (layoutV2Result.rows.length === 0) {
|
||||
logger.info(` ↳ V2 레이아웃 없음 (스킵): screen_id=${originalScreenId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 모든 레이어에서 컴포넌트를 수집하여 componentIdMap 생성
|
||||
const componentIdMap = new Map<string, string>();
|
||||
const timestamp = Date.now();
|
||||
let compIdx = 0;
|
||||
for (const layer of layoutV2Result.rows) {
|
||||
const components = layer.layout_data?.components || [];
|
||||
for (const comp of components) {
|
||||
if (!componentIdMap.has(comp.id)) {
|
||||
const newId = `comp_${timestamp}_${compIdx++}_${Math.random().toString(36).substr(2, 5)}`;
|
||||
componentIdMap.set(comp.id, newId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// screen_conditional_zones 복제 + zoneIdMap 생성
|
||||
const zoneIdMap = new Map<number, number>();
|
||||
const zonesResult = await client.query(
|
||||
`SELECT * FROM screen_conditional_zones WHERE screen_id = $1`,
|
||||
[originalScreenId]
|
||||
);
|
||||
|
||||
const layoutData = layoutV2Result.rows[0]?.layout_data;
|
||||
const components = layoutData?.components || [];
|
||||
if (isUpdate) {
|
||||
await client.query(
|
||||
`DELETE FROM screen_conditional_zones WHERE screen_id = $1 AND company_code = $2`,
|
||||
[targetScreenId, targetCompanyCode]
|
||||
);
|
||||
}
|
||||
|
||||
if (layoutData && components.length > 0) {
|
||||
// component_id 매핑 생성 (원본 → 새 ID)
|
||||
const componentIdMap = new Map<string, string>();
|
||||
const timestamp = Date.now();
|
||||
components.forEach((comp: any, idx: number) => {
|
||||
const newComponentId = `comp_${timestamp}_${idx}_${Math.random().toString(36).substr(2, 5)}`;
|
||||
componentIdMap.set(comp.id, newComponentId);
|
||||
});
|
||||
for (const zone of zonesResult.rows) {
|
||||
const newTriggerCompId = componentIdMap.get(zone.trigger_component_id) || zone.trigger_component_id;
|
||||
const newZone = await client.query<{ zone_id: number }>(
|
||||
`INSERT INTO screen_conditional_zones
|
||||
(screen_id, company_code, zone_name, x, y, width, height,
|
||||
trigger_component_id, trigger_operator)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING zone_id`,
|
||||
[targetScreenId, targetCompanyCode, zone.zone_name,
|
||||
zone.x, zone.y, zone.width, zone.height,
|
||||
newTriggerCompId, zone.trigger_operator]
|
||||
);
|
||||
zoneIdMap.set(zone.zone_id, newZone.rows[0].zone_id);
|
||||
}
|
||||
|
||||
if (zonesResult.rows.length > 0) {
|
||||
logger.info(` ↳ 조건부 영역 복사: ${zonesResult.rows.length}개 (zoneIdMap: ${zoneIdMap.size}개)`);
|
||||
}
|
||||
|
||||
// 업데이트인 경우 기존 레이아웃 삭제 (레이어 수 변경 대응)
|
||||
if (isUpdate) {
|
||||
await client.query(
|
||||
`DELETE FROM screen_layouts_v2 WHERE screen_id = $1 AND company_code = $2`,
|
||||
[targetScreenId, targetCompanyCode]
|
||||
);
|
||||
}
|
||||
|
||||
// 각 레이어별 처리
|
||||
let totalComponents = 0;
|
||||
for (const layer of layoutV2Result.rows) {
|
||||
const layoutData = layer.layout_data;
|
||||
const components = layoutData?.components || [];
|
||||
|
||||
if (!layoutData || components.length === 0) continue;
|
||||
totalComponents += components.length;
|
||||
|
||||
// V2 레이아웃 데이터 복사 및 참조 업데이트
|
||||
const updatedLayoutData = this.updateReferencesInLayoutDataV2(
|
||||
@@ -1692,20 +1811,34 @@ export class MenuCopyService {
|
||||
menuIdMap
|
||||
);
|
||||
|
||||
// V2 레이아웃 저장 (UPSERT)
|
||||
await client.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()`,
|
||||
[targetScreenId, targetCompanyCode, JSON.stringify(updatedLayoutData)]
|
||||
);
|
||||
// condition_config의 zone_id 재매핑
|
||||
let updatedConditionConfig = layer.condition_config ? { ...layer.condition_config } : null;
|
||||
if (updatedConditionConfig?.zone_id) {
|
||||
const newZoneId = zoneIdMap.get(updatedConditionConfig.zone_id);
|
||||
if (newZoneId) {
|
||||
updatedConditionConfig.zone_id = newZoneId;
|
||||
}
|
||||
}
|
||||
|
||||
const action = isUpdate ? "업데이트" : "복사";
|
||||
logger.info(` ↳ V2 레이아웃 ${action}: ${components.length}개 컴포넌트`);
|
||||
} else {
|
||||
logger.info(` ↳ V2 레이아웃 없음 (스킵): screen_id=${originalScreenId}`);
|
||||
// V2 레이아웃 저장 (레이어별 INSERT)
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, condition_config, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id)
|
||||
DO UPDATE SET layout_data = $5, layer_name = $4, condition_config = $6, updated_at = NOW()`,
|
||||
[
|
||||
targetScreenId,
|
||||
targetCompanyCode,
|
||||
layer.layer_id,
|
||||
layer.layer_name,
|
||||
JSON.stringify(updatedLayoutData),
|
||||
updatedConditionConfig ? JSON.stringify(updatedConditionConfig) : null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const action = isUpdate ? "업데이트" : "복사";
|
||||
logger.info(` ↳ V2 레이아웃 ${action}: ${layoutV2Result.rows.length}개 레이어, ${totalComponents}개 컴포넌트`);
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`❌ V2 레이아웃 처리 실패: screen_id=${originalScreenId}`,
|
||||
@@ -1985,6 +2118,26 @@ export class MenuCopyService {
|
||||
|
||||
logger.info(`📂 메뉴 복사 중: ${menus.length}개`);
|
||||
|
||||
// screen_group_id 재매핑 맵 생성 (source company → target company)
|
||||
const screenGroupIdMap = new Map<number, number>();
|
||||
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);
|
||||
|
||||
@@ -2120,7 +2273,7 @@ export class MenuCopyService {
|
||||
menu.menu_url,
|
||||
menu.menu_desc,
|
||||
userId,
|
||||
'active',
|
||||
menu.status || 'active',
|
||||
menu.system_name,
|
||||
targetCompanyCode,
|
||||
menu.lang_key,
|
||||
@@ -2129,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,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2250,8 +2403,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>,
|
||||
@@ -2259,56 +2413,197 @@ 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}개`);
|
||||
}
|
||||
|
||||
/**
|
||||
* screen_group_screens 복제 (화면-스크린그룹 매핑)
|
||||
*/
|
||||
private async copyScreenGroupScreens(
|
||||
screenIds: Set<number>,
|
||||
screenIdMap: Map<number, number>,
|
||||
sourceCompanyCode: string,
|
||||
targetCompanyCode: string,
|
||||
client: PoolClient
|
||||
): Promise<void> {
|
||||
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<number, number>();
|
||||
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}개`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -4210,39 +4276,65 @@ export class ScreenManagementService {
|
||||
|
||||
const newScreen = newScreenResult.rows[0];
|
||||
|
||||
// 4. 원본 화면의 V2 레이아웃 조회
|
||||
let sourceLayoutV2Result = await client.query<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2`,
|
||||
// 4. 원본 화면의 V2 레이아웃 전체 조회 (모든 레이어)
|
||||
let sourceLayoutV2Result = await client.query<{
|
||||
layout_data: any;
|
||||
layer_id: number;
|
||||
layer_name: string;
|
||||
condition_config: any;
|
||||
}>(
|
||||
`SELECT layout_data, layer_id, layer_name, condition_config
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = $2
|
||||
ORDER BY layer_id`,
|
||||
[sourceScreenId, sourceScreen.company_code],
|
||||
);
|
||||
|
||||
// 없으면 공통(*) 레이아웃 조회
|
||||
let layoutData = sourceLayoutV2Result.rows[0]?.layout_data;
|
||||
if (!layoutData && sourceScreen.company_code !== "*") {
|
||||
const fallbackResult = await client.query<{ layout_data: any }>(
|
||||
`SELECT layout_data FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = '*'`,
|
||||
if (sourceLayoutV2Result.rows.length === 0 && sourceScreen.company_code !== "*") {
|
||||
sourceLayoutV2Result = await client.query<{
|
||||
layout_data: any;
|
||||
layer_id: number;
|
||||
layer_name: string;
|
||||
condition_config: any;
|
||||
}>(
|
||||
`SELECT layout_data, layer_id, layer_name, condition_config
|
||||
FROM screen_layouts_v2
|
||||
WHERE screen_id = $1 AND company_code = '*'
|
||||
ORDER BY layer_id`,
|
||||
[sourceScreenId],
|
||||
);
|
||||
layoutData = fallbackResult.rows[0]?.layout_data;
|
||||
}
|
||||
|
||||
const components = layoutData?.components || [];
|
||||
// 모든 레이어에서 컴포넌트를 수집하여 componentIdMap 생성
|
||||
const componentIdMap = new Map<string, string>();
|
||||
for (const layer of sourceLayoutV2Result.rows) {
|
||||
const components = layer.layout_data?.components || [];
|
||||
for (const comp of components) {
|
||||
if (!componentIdMap.has(comp.id)) {
|
||||
componentIdMap.set(comp.id, generateId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasComponents = componentIdMap.size > 0;
|
||||
// 첫 번째 레이어의 layoutData (flowId/ruleId 수집용 - 모든 레이어에서 수집)
|
||||
const allLayoutDatas = sourceLayoutV2Result.rows.map((r: any) => r.layout_data).filter(Boolean);
|
||||
|
||||
// 5. 노드 플로우 복사 (회사가 다른 경우)
|
||||
let flowIdMap = new Map<number, number>();
|
||||
if (
|
||||
components.length > 0 &&
|
||||
hasComponents &&
|
||||
sourceScreen.company_code !== targetCompanyCode
|
||||
) {
|
||||
// V2 레이아웃에서 flowId 수집
|
||||
const flowIds = this.collectFlowIdsFromLayoutData(layoutData);
|
||||
const flowIds = new Set<number>();
|
||||
for (const ld of allLayoutDatas) {
|
||||
const ids = this.collectFlowIdsFromLayoutData(ld);
|
||||
ids.forEach((id: number) => flowIds.add(id));
|
||||
}
|
||||
|
||||
if (flowIds.size > 0) {
|
||||
console.log(`🔍 화면 복사 - flowId 수집: ${flowIds.size}개`);
|
||||
|
||||
// 노드 플로우 복사 및 매핑 생성
|
||||
flowIdMap = await this.copyNodeFlowsForScreen(
|
||||
flowIds,
|
||||
sourceScreen.company_code,
|
||||
@@ -4255,16 +4347,17 @@ export class ScreenManagementService {
|
||||
// 5.1. 채번 규칙 복사 (회사가 다른 경우)
|
||||
let ruleIdMap = new Map<string, string>();
|
||||
if (
|
||||
components.length > 0 &&
|
||||
hasComponents &&
|
||||
sourceScreen.company_code !== targetCompanyCode
|
||||
) {
|
||||
// V2 레이아웃에서 채번 규칙 ID 수집
|
||||
const ruleIds = this.collectNumberingRuleIdsFromLayoutData(layoutData);
|
||||
const ruleIds = new Set<string>();
|
||||
for (const ld of allLayoutDatas) {
|
||||
const ids = this.collectNumberingRuleIdsFromLayoutData(ld);
|
||||
ids.forEach((id: string) => ruleIds.add(id));
|
||||
}
|
||||
|
||||
if (ruleIds.size > 0) {
|
||||
console.log(`🔍 화면 복사 - 채번 규칙 ID 수집: ${ruleIds.size}개`);
|
||||
|
||||
// 채번 규칙 복사 및 매핑 생성
|
||||
ruleIdMap = await this.copyNumberingRulesForScreen(
|
||||
ruleIds,
|
||||
sourceScreen.company_code,
|
||||
@@ -4274,39 +4367,89 @@ export class ScreenManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. V2 레이아웃이 있다면 복사
|
||||
if (layoutData && components.length > 0) {
|
||||
// 5.2. screen_conditional_zones 복제 + zoneIdMap 생성
|
||||
const zoneIdMap = new Map<number, number>();
|
||||
if (hasComponents) {
|
||||
try {
|
||||
// componentId 매핑 생성
|
||||
const componentIdMap = new Map<string, string>();
|
||||
for (const comp of components) {
|
||||
componentIdMap.set(comp.id, generateId());
|
||||
const zonesResult = await client.query(
|
||||
`SELECT * FROM screen_conditional_zones WHERE screen_id = $1`,
|
||||
[sourceScreenId]
|
||||
);
|
||||
|
||||
for (const zone of zonesResult.rows) {
|
||||
const newTriggerCompId = componentIdMap.get(zone.trigger_component_id) || zone.trigger_component_id;
|
||||
const newZone = await client.query<{ zone_id: number }>(
|
||||
`INSERT INTO screen_conditional_zones
|
||||
(screen_id, company_code, zone_name, x, y, width, height,
|
||||
trigger_component_id, trigger_operator)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING zone_id`,
|
||||
[newScreen.screen_id, targetCompanyCode, zone.zone_name,
|
||||
zone.x, zone.y, zone.width, zone.height,
|
||||
newTriggerCompId, zone.trigger_operator]
|
||||
);
|
||||
zoneIdMap.set(zone.zone_id, newZone.rows[0].zone_id);
|
||||
}
|
||||
|
||||
// V2 레이아웃 데이터 복사 및 참조 업데이트
|
||||
const updatedLayoutData = this.updateReferencesInLayoutData(
|
||||
layoutData,
|
||||
{
|
||||
componentIdMap,
|
||||
flowIdMap: flowIdMap.size > 0 ? flowIdMap : undefined,
|
||||
ruleIdMap: ruleIdMap.size > 0 ? ruleIdMap : undefined,
|
||||
// screenIdMap은 모든 화면 복제 완료 후 updateTabScreenReferences에서 일괄 처리
|
||||
},
|
||||
);
|
||||
if (zonesResult.rows.length > 0) {
|
||||
console.log(` ↳ 조건부 영역 복사: ${zonesResult.rows.length}개`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("조건부 영역 복사 중 오류:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// V2 레이아웃 저장 (UPSERT) - layer_id 포함
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layout_data, created_at, updated_at)
|
||||
VALUES ($1, $2, 1, $3, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id)
|
||||
DO UPDATE SET layout_data = $3, updated_at = NOW()`,
|
||||
[newScreen.screen_id, targetCompanyCode, JSON.stringify(updatedLayoutData)],
|
||||
);
|
||||
// 6. V2 레이아웃 복사 (모든 레이어 순회)
|
||||
if (sourceLayoutV2Result.rows.length > 0 && hasComponents) {
|
||||
try {
|
||||
let totalComponents = 0;
|
||||
|
||||
|
||||
for (const layer of sourceLayoutV2Result.rows) {
|
||||
const layoutData = layer.layout_data;
|
||||
const components = layoutData?.components || [];
|
||||
|
||||
if (!layoutData || components.length === 0) continue;
|
||||
totalComponents += components.length;
|
||||
|
||||
// V2 레이아웃 데이터 복사 및 참조 업데이트
|
||||
const updatedLayoutData = this.updateReferencesInLayoutData(
|
||||
layoutData,
|
||||
{
|
||||
componentIdMap,
|
||||
flowIdMap: flowIdMap.size > 0 ? flowIdMap : undefined,
|
||||
ruleIdMap: ruleIdMap.size > 0 ? ruleIdMap : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
// condition_config의 zone_id 재매핑
|
||||
let updatedConditionConfig = layer.condition_config ? { ...layer.condition_config } : null;
|
||||
if (updatedConditionConfig?.zone_id) {
|
||||
const newZoneId = zoneIdMap.get(updatedConditionConfig.zone_id);
|
||||
if (newZoneId) {
|
||||
updatedConditionConfig.zone_id = newZoneId;
|
||||
}
|
||||
}
|
||||
|
||||
// V2 레이아웃 저장 (레이어별 INSERT)
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts_v2 (screen_id, company_code, layer_id, layer_name, layout_data, condition_config, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
ON CONFLICT (screen_id, company_code, layer_id)
|
||||
DO UPDATE SET layout_data = $5, layer_name = $4, condition_config = $6, updated_at = NOW()`,
|
||||
[
|
||||
newScreen.screen_id,
|
||||
targetCompanyCode,
|
||||
layer.layer_id,
|
||||
layer.layer_name,
|
||||
JSON.stringify(updatedLayoutData),
|
||||
updatedConditionConfig ? JSON.stringify(updatedConditionConfig) : null,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
console.log(` ↳ V2 레이아웃 복사: ${sourceLayoutV2Result.rows.length}개 레이어, ${totalComponents}개 컴포넌트`);
|
||||
} catch (error) {
|
||||
console.error("V2 레이아웃 복사 중 오류:", error);
|
||||
// 레이아웃 복사 실패해도 화면 생성은 유지
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4533,9 +4676,60 @@ export class ScreenManagementService {
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 총 ${updateCount}개 레이아웃의 연결된 화면 ID 업데이트 완료 (버튼 + 조건부컨테이너)`,
|
||||
`✅ V1: ${updateCount}개 레이아웃 업데이트 완료`,
|
||||
);
|
||||
return updateCount;
|
||||
|
||||
// V2 레이아웃(screen_layouts_v2)에서도 targetScreenId 등 재매핑
|
||||
const v2Layouts = await query<any>(
|
||||
`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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user