fix: 멀티 레이어 화면 복제 버그 수정
- menuCopyService: 모든 레이어 순회 처리, ON CONFLICT 제약조건 수정 - screenManagementService: layer_id 하드코딩 제거, 멀티 레이어 순회 - screen_conditional_zones 복제 로직 추가 (zoneIdMap 생성) - condition_config의 zone_id 재매핑 로직 추가 - 레이어 수 변경 감지를 위한 변경 비교 로직 보강 Made-with: Cursor
This commit is contained in:
parent
e11a7b1237
commit
8d0f2dbb27
|
|
@ -1535,20 +1535,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 +1653,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 +1665,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 +1760,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}`,
|
||||
|
|
|
|||
|
|
@ -4210,39 +4210,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 +4281,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 +4301,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);
|
||||
// 레이아웃 복사 실패해도 화면 생성은 유지
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue