diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index eb230454..ac9768a1 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -938,7 +938,9 @@ export class MenuCopyService { copiedCategoryMappings = await this.copyCategoryMappingsAndValues( menuObjids, menuIdMap, + sourceCompanyCode, targetCompanyCode, + Array.from(screenIds), userId, client ); @@ -2569,11 +2571,16 @@ export class MenuCopyService { /** * 카테고리 매핑 + 값 복사 (최적화: 배치 조회) + * + * 화면에서 사용하는 table_name + column_name 조합을 기준으로 카테고리 값 복사 + * menu_objid 기준이 아닌 화면 컴포넌트 기준으로 복사하여 누락 방지 */ private async copyCategoryMappingsAndValues( menuObjids: number[], menuIdMap: Map, + sourceCompanyCode: string, targetCompanyCode: string, + screenIds: number[], userId: string, client: PoolClient ): Promise { @@ -2697,12 +2704,70 @@ export class MenuCopyService { ); } - // 4. 모든 원본 카테고리 값 한 번에 조회 + // 4. 화면에서 사용하는 카테고리 컬럼 조합 수집 + // 복사된 화면의 레이아웃에서 webType='category'인 컴포넌트의 tableName, columnName 추출 + const categoryColumnsResult = await client.query( + `SELECT DISTINCT + sl.properties->>'tableName' as table_name, + sl.properties->>'columnName' as column_name + FROM screen_layouts sl + JOIN screen_definitions sd ON sl.screen_id = sd.screen_id + WHERE sd.screen_id = ANY($1) + AND sl.properties->'componentConfig'->>'webType' = 'category' + AND sl.properties->>'tableName' IS NOT NULL + AND sl.properties->>'columnName' IS NOT NULL`, + [screenIds] + ); + + // 카테고리 매핑에서 사용하는 table_name, column_name도 추가 + const mappingColumnsResult = await client.query( + `SELECT DISTINCT table_name, logical_column_name as column_name + FROM category_column_mapping + WHERE menu_objid = ANY($1)`, + [menuObjids] + ); + + // 두 결과 합치기 + const categoryColumns = new Set(); + for (const row of categoryColumnsResult.rows) { + if (row.table_name && row.column_name) { + categoryColumns.add(`${row.table_name}|${row.column_name}`); + } + } + for (const row of mappingColumnsResult.rows) { + if (row.table_name && row.column_name) { + categoryColumns.add(`${row.table_name}|${row.column_name}`); + } + } + + logger.info( + ` 📋 화면에서 사용하는 카테고리 컬럼: ${categoryColumns.size}개` + ); + + if (categoryColumns.size === 0) { + logger.info(`✅ 카테고리 매핑 + 값 복사 완료: ${copiedCount}개`); + return copiedCount; + } + + // 5. 원본 회사의 카테고리 값 조회 (table_name + column_name 기준) + // menu_objid 조건 대신 table_name + column_name + 원본 회사 코드로 조회 + const columnConditions = Array.from(categoryColumns).map((col, i) => { + const [tableName, columnName] = col.split("|"); + return `(table_name = $${i * 2 + 2} AND column_name = $${i * 2 + 3})`; + }); + + const columnParams: string[] = []; + for (const col of categoryColumns) { + const [tableName, columnName] = col.split("|"); + columnParams.push(tableName, columnName); + } + const allValuesResult = await client.query( `SELECT * FROM table_column_category_values - WHERE menu_objid = ANY($1) + WHERE company_code = $1 + AND (${columnConditions.join(" OR ")}) ORDER BY depth NULLS FIRST, parent_value_id NULLS FIRST, value_order`, - [menuObjids] + [sourceCompanyCode, ...columnParams] ); if (allValuesResult.rows.length === 0) { @@ -2710,6 +2775,8 @@ export class MenuCopyService { return copiedCount; } + logger.info(` 📋 원본 카테고리 값: ${allValuesResult.rows.length}개 발견`); + // 5. 대상 회사에 이미 존재하는 값 한 번에 조회 const existingValuesResult = await client.query( `SELECT value_id, table_name, column_name, value_code @@ -2763,8 +2830,12 @@ export class MenuCopyService { ) .join(", "); + // 기본 menu_objid: 매핑이 없을 경우 첫 번째 복사된 메뉴 사용 + const defaultMenuObjid = menuIdMap.values().next().value || 0; + const valueParams = values.flatMap((v) => { - const newMenuObjid = menuIdMap.get(v.menu_objid); + // 원본 menu_objid가 매핑에 있으면 사용, 없으면 기본값 사용 + const newMenuObjid = menuIdMap.get(v.menu_objid) ?? defaultMenuObjid; const newParentId = v.parent_value_id ? valueIdMap.get(v.parent_value_id) || null : null;