Merge main into feature/screen-management (menuCopyService 충돌 해결)
This commit is contained in:
@@ -22,6 +22,15 @@ const router = Router();
|
||||
// 모든 role 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* 사용자 권한 그룹 조회 (/:id 보다 먼저 정의해야 함)
|
||||
*/
|
||||
// 현재 사용자가 속한 권한 그룹 조회
|
||||
router.get("/user/my-groups", getUserRoleGroups);
|
||||
|
||||
// 특정 사용자가 속한 권한 그룹 조회
|
||||
router.get("/user/:userId/groups", requireAdmin, getUserRoleGroups);
|
||||
|
||||
/**
|
||||
* 권한 그룹 CRUD
|
||||
*/
|
||||
@@ -67,13 +76,4 @@ router.get("/:id/menu-permissions", requireAdmin, getMenuPermissions);
|
||||
// 메뉴 권한 설정
|
||||
router.put("/:id/menu-permissions", requireAdmin, setMenuPermissions);
|
||||
|
||||
/**
|
||||
* 사용자 권한 그룹 조회
|
||||
*/
|
||||
// 현재 사용자가 속한 권한 그룹 조회
|
||||
router.get("/user/my-groups", getUserRoleGroups);
|
||||
|
||||
// 특정 사용자가 속한 권한 그룹 조회
|
||||
router.get("/user/:userId/groups", requireAdmin, getUserRoleGroups);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -299,18 +299,16 @@ export class MenuCopyService {
|
||||
const screenIds = new Set<number>();
|
||||
const visited = new Set<number>();
|
||||
|
||||
// 1) 메뉴에 직접 할당된 화면
|
||||
for (const menuObjid of menuObjids) {
|
||||
const assignmentsResult = await client.query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id
|
||||
FROM screen_menu_assignments
|
||||
WHERE menu_objid = $1 AND company_code = $2`,
|
||||
[menuObjid, sourceCompanyCode]
|
||||
);
|
||||
// 1) 메뉴에 직접 할당된 화면 - 배치 조회
|
||||
const assignmentsResult = await client.query<{ screen_id: number }>(
|
||||
`SELECT DISTINCT screen_id
|
||||
FROM screen_menu_assignments
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2`,
|
||||
[menuObjids, sourceCompanyCode]
|
||||
);
|
||||
|
||||
for (const assignment of assignmentsResult.rows) {
|
||||
screenIds.add(assignment.screen_id);
|
||||
}
|
||||
for (const assignment of assignmentsResult.rows) {
|
||||
screenIds.add(assignment.screen_id);
|
||||
}
|
||||
|
||||
logger.info(`📌 직접 할당 화면: ${screenIds.size}개`);
|
||||
@@ -371,50 +369,56 @@ export class MenuCopyService {
|
||||
screenId: number;
|
||||
}> = [];
|
||||
|
||||
for (const screenId of screenIds) {
|
||||
const layoutsResult = await client.query<ScreenLayout>(
|
||||
`SELECT properties FROM screen_layouts WHERE screen_id = $1`,
|
||||
[screenId]
|
||||
);
|
||||
// 배치 조회: 모든 화면의 레이아웃을 한 번에 조회
|
||||
const screenIdArray = Array.from(screenIds);
|
||||
if (screenIdArray.length === 0) {
|
||||
return flowIds;
|
||||
}
|
||||
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const props = layout.properties;
|
||||
const layoutsResult = await client.query<
|
||||
ScreenLayout & { screen_id: number }
|
||||
>(
|
||||
`SELECT screen_id, properties FROM screen_layouts WHERE screen_id = ANY($1)`,
|
||||
[screenIdArray]
|
||||
);
|
||||
|
||||
// webTypeConfig.dataflowConfig.flowConfig.flowId
|
||||
const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
|
||||
const flowName =
|
||||
props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowName ||
|
||||
"Unknown";
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const props = layout.properties;
|
||||
const screenId = layout.screen_id;
|
||||
|
||||
if (flowId && typeof flowId === "number" && flowId > 0) {
|
||||
if (!flowIds.has(flowId)) {
|
||||
flowIds.add(flowId);
|
||||
flowDetails.push({ flowId, flowName, screenId });
|
||||
logger.info(
|
||||
` 📎 화면 ${screenId}에서 플로우 발견: id=${flowId}, name="${flowName}"`
|
||||
);
|
||||
}
|
||||
// webTypeConfig.dataflowConfig.flowConfig.flowId
|
||||
const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId;
|
||||
const flowName =
|
||||
props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowName || "Unknown";
|
||||
|
||||
if (flowId && typeof flowId === "number" && flowId > 0) {
|
||||
if (!flowIds.has(flowId)) {
|
||||
flowIds.add(flowId);
|
||||
flowDetails.push({ flowId, flowName, screenId });
|
||||
logger.info(
|
||||
` 📎 화면 ${screenId}에서 플로우 발견: id=${flowId}, name="${flowName}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// selectedDiagramId도 확인 (flowId와 동일할 수 있지만 다를 수도 있음)
|
||||
const selectedDiagramId =
|
||||
props?.webTypeConfig?.dataflowConfig?.selectedDiagramId;
|
||||
if (
|
||||
selectedDiagramId &&
|
||||
typeof selectedDiagramId === "number" &&
|
||||
selectedDiagramId > 0
|
||||
) {
|
||||
if (!flowIds.has(selectedDiagramId)) {
|
||||
flowIds.add(selectedDiagramId);
|
||||
flowDetails.push({
|
||||
flowId: selectedDiagramId,
|
||||
flowName: "SelectedDiagram",
|
||||
screenId,
|
||||
});
|
||||
logger.info(
|
||||
` 📎 화면 ${screenId}에서 selectedDiagramId 발견: id=${selectedDiagramId}`
|
||||
);
|
||||
}
|
||||
// selectedDiagramId도 확인 (flowId와 동일할 수 있지만 다를 수도 있음)
|
||||
const selectedDiagramId =
|
||||
props?.webTypeConfig?.dataflowConfig?.selectedDiagramId;
|
||||
if (
|
||||
selectedDiagramId &&
|
||||
typeof selectedDiagramId === "number" &&
|
||||
selectedDiagramId > 0
|
||||
) {
|
||||
if (!flowIds.has(selectedDiagramId)) {
|
||||
flowIds.add(selectedDiagramId);
|
||||
flowDetails.push({
|
||||
flowId: selectedDiagramId,
|
||||
flowName: "SelectedDiagram",
|
||||
screenId,
|
||||
});
|
||||
logger.info(
|
||||
` 📎 화면 ${screenId}에서 selectedDiagramId 발견: id=${selectedDiagramId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,6 +586,12 @@ export class MenuCopyService {
|
||||
logger.info(
|
||||
` 🔗 채번규칙 참조 업데이트 (${currentPath}): ${value} → ${newRuleId}`
|
||||
);
|
||||
} else {
|
||||
// 매핑이 없는 채번규칙은 빈 값으로 설정 (다른 회사 채번규칙 참조 방지)
|
||||
logger.warn(
|
||||
` ⚠️ 채번규칙 매핑 없음 (${currentPath}): ${value} → 빈 값으로 설정`
|
||||
);
|
||||
obj[key] = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +738,7 @@ export class MenuCopyService {
|
||||
]);
|
||||
logger.info(` ✅ 메뉴 권한 삭제 완료`);
|
||||
|
||||
// 5-4. 채번 규칙 처리 (외래키 제약조건 해결)
|
||||
// 5-4. 채번 규칙 처리 (체크 제약조건 고려)
|
||||
// scope_type = 'menu'인 채번 규칙: 메뉴 전용이므로 삭제 (파트 포함)
|
||||
// check_menu_scope_requires_menu_objid 제약: scope_type='menu'이면 menu_objid NOT NULL 필수
|
||||
const menuScopedRulesResult = await client.query(
|
||||
@@ -746,47 +756,53 @@ export class MenuCopyService {
|
||||
[menuScopedRuleIds]
|
||||
);
|
||||
// 채번 규칙 삭제
|
||||
await client.query(
|
||||
`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`,
|
||||
[menuScopedRuleIds]
|
||||
);
|
||||
await client.query(`DELETE FROM numbering_rules WHERE rule_id = ANY($1)`, [
|
||||
menuScopedRuleIds,
|
||||
]);
|
||||
logger.info(
|
||||
` ✅ 메뉴 전용 채번 규칙 삭제: ${menuScopedRuleIds.length}개`
|
||||
);
|
||||
}
|
||||
|
||||
// scope_type != 'menu'인 채번 규칙: menu_objid만 NULL로 설정 (규칙 보존)
|
||||
const tableScopedRulesResult = await client.query(
|
||||
`UPDATE numbering_rules
|
||||
const updatedNumberingRules = await client.query(
|
||||
`UPDATE numbering_rules
|
||||
SET menu_objid = NULL
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2
|
||||
AND (scope_type IS NULL OR scope_type != 'menu')
|
||||
RETURNING rule_id`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
if (tableScopedRulesResult.rows.length > 0) {
|
||||
if (updatedNumberingRules.rowCount && updatedNumberingRules.rowCount > 0) {
|
||||
logger.info(
|
||||
` ✅ 테이블 스코프 채번 규칙 연결 해제: ${tableScopedRulesResult.rows.length}개 (데이터 보존)`
|
||||
` ✅ 테이블 스코프 채번 규칙 연결 해제: ${updatedNumberingRules.rowCount}개 (데이터 보존됨)`
|
||||
);
|
||||
}
|
||||
|
||||
// 5-5. 카테고리 컬럼 매핑 삭제 (NOT NULL 제약조건으로 삭제 필요)
|
||||
// 5-5. 카테고리 매핑 삭제 (menu_objid가 NOT NULL이므로 NULL 설정 불가)
|
||||
// 카테고리 매핑은 메뉴와 강하게 연결되어 있으므로 함께 삭제
|
||||
const deletedCategoryMappings = await client.query(
|
||||
`DELETE FROM category_column_mapping
|
||||
`DELETE FROM category_column_mapping
|
||||
WHERE menu_objid = ANY($1) AND company_code = $2
|
||||
RETURNING mapping_id`,
|
||||
[existingMenuIds, targetCompanyCode]
|
||||
);
|
||||
if (deletedCategoryMappings.rows.length > 0) {
|
||||
if (
|
||||
deletedCategoryMappings.rowCount &&
|
||||
deletedCategoryMappings.rowCount > 0
|
||||
) {
|
||||
logger.info(
|
||||
` ✅ 카테고리 매핑 삭제: ${deletedCategoryMappings.rows.length}개`
|
||||
` ✅ 카테고리 매핑 삭제 완료: ${deletedCategoryMappings.rowCount}개`
|
||||
);
|
||||
}
|
||||
|
||||
// 5-6. 메뉴 삭제 (배치)
|
||||
await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [
|
||||
existingMenuIds,
|
||||
]);
|
||||
// 5-6. 메뉴 삭제 (배치 삭제 - 하위 메뉴부터 삭제를 위해 역순 정렬된 ID 사용)
|
||||
// 외래키 제약이 해제되었으므로 배치 삭제 가능
|
||||
if (existingMenuIds.length > 0) {
|
||||
await client.query(`DELETE FROM menu_info WHERE objid = ANY($1)`, [
|
||||
existingMenuIds,
|
||||
]);
|
||||
}
|
||||
logger.info(` ✅ 메뉴 삭제 완료: ${existingMenus.length}개`);
|
||||
|
||||
logger.info("✅ 기존 복사본 삭제 완료 - 덮어쓰기 준비됨");
|
||||
@@ -939,6 +955,20 @@ export class MenuCopyService {
|
||||
);
|
||||
}
|
||||
|
||||
// === 4.9단계: 화면에서 참조하는 채번규칙 매핑 보완 ===
|
||||
// 화면 properties에서 참조하는 채번규칙 중 아직 매핑되지 않은 것들을
|
||||
// 대상 회사에서 같은 이름의 채번규칙으로 매핑
|
||||
if (screenIds.size > 0) {
|
||||
logger.info("\n🔗 [4.9단계] 화면 채번규칙 참조 매핑 보완");
|
||||
await this.supplementNumberingRuleMapping(
|
||||
Array.from(screenIds),
|
||||
sourceCompanyCode,
|
||||
targetCompanyCode,
|
||||
numberingRuleIdMap,
|
||||
client
|
||||
);
|
||||
}
|
||||
|
||||
// === 5단계: 화면 복사 ===
|
||||
logger.info("\n📄 [5단계] 화면 복사");
|
||||
const screenIdMap = await this.copyScreens(
|
||||
@@ -1293,6 +1323,37 @@ export class MenuCopyService {
|
||||
|
||||
logger.info(`📄 화면 복사/업데이트 중: ${screenIds.size}개`);
|
||||
|
||||
// === 0단계: 원본 화면 정의 배치 조회 ===
|
||||
const screenIdArray = Array.from(screenIds);
|
||||
const allScreenDefsResult = await client.query<ScreenDefinition>(
|
||||
`SELECT * FROM screen_definitions WHERE screen_id = ANY($1)`,
|
||||
[screenIdArray]
|
||||
);
|
||||
const screenDefMap = new Map<number, ScreenDefinition>();
|
||||
for (const def of allScreenDefsResult.rows) {
|
||||
screenDefMap.set(def.screen_id, def);
|
||||
}
|
||||
|
||||
// 대상 회사의 기존 복사본 배치 조회 (source_screen_id 기준)
|
||||
const existingCopiesResult = await client.query<{
|
||||
screen_id: number;
|
||||
screen_name: string;
|
||||
source_screen_id: number;
|
||||
updated_date: Date;
|
||||
}>(
|
||||
`SELECT screen_id, screen_name, source_screen_id, updated_date
|
||||
FROM screen_definitions
|
||||
WHERE source_screen_id = ANY($1) AND company_code = $2 AND deleted_date IS NULL`,
|
||||
[screenIdArray, targetCompanyCode]
|
||||
);
|
||||
const existingCopyMap = new Map<
|
||||
number,
|
||||
{ screen_id: number; screen_name: string; updated_date: Date }
|
||||
>();
|
||||
for (const copy of existingCopiesResult.rows) {
|
||||
existingCopyMap.set(copy.source_screen_id, copy);
|
||||
}
|
||||
|
||||
// === 1단계: 모든 screen_definitions 처리 (screenIdMap 생성) ===
|
||||
const screenDefsToProcess: Array<{
|
||||
originalScreenId: number;
|
||||
@@ -1303,35 +1364,20 @@ export class MenuCopyService {
|
||||
|
||||
for (const originalScreenId of screenIds) {
|
||||
try {
|
||||
// 1) 원본 screen_definitions 조회
|
||||
const screenDefResult = await client.query<ScreenDefinition>(
|
||||
`SELECT * FROM screen_definitions WHERE screen_id = $1`,
|
||||
[originalScreenId]
|
||||
);
|
||||
// 1) 원본 screen_definitions 조회 (캐시에서)
|
||||
const screenDef = screenDefMap.get(originalScreenId);
|
||||
|
||||
if (screenDefResult.rows.length === 0) {
|
||||
if (!screenDef) {
|
||||
logger.warn(`⚠️ 화면을 찾을 수 없음: screen_id=${originalScreenId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenDef = screenDefResult.rows[0];
|
||||
|
||||
// 2) 기존 복사본 찾기: source_screen_id로 검색
|
||||
let existingCopyResult = await client.query<{
|
||||
screen_id: number;
|
||||
screen_name: string;
|
||||
updated_date: Date;
|
||||
}>(
|
||||
`SELECT screen_id, screen_name, updated_date
|
||||
FROM screen_definitions
|
||||
WHERE source_screen_id = $1 AND company_code = $2 AND deleted_date IS NULL
|
||||
LIMIT 1`,
|
||||
[originalScreenId, targetCompanyCode]
|
||||
);
|
||||
// 2) 기존 복사본 찾기: 캐시에서 조회 (source_screen_id 기준)
|
||||
let existingCopy = existingCopyMap.get(originalScreenId);
|
||||
|
||||
// 2-1) source_screen_id가 없는 기존 복사본 (이름 + 테이블로 검색) - 호환성 유지
|
||||
if (existingCopyResult.rows.length === 0 && screenDef.screen_name) {
|
||||
existingCopyResult = await client.query<{
|
||||
if (!existingCopy && screenDef.screen_name) {
|
||||
const legacyCopyResult = await client.query<{
|
||||
screen_id: number;
|
||||
screen_name: string;
|
||||
updated_date: Date;
|
||||
@@ -1347,14 +1393,15 @@ export class MenuCopyService {
|
||||
[screenDef.screen_name, screenDef.table_name, targetCompanyCode]
|
||||
);
|
||||
|
||||
if (existingCopyResult.rows.length > 0) {
|
||||
if (legacyCopyResult.rows.length > 0) {
|
||||
existingCopy = legacyCopyResult.rows[0];
|
||||
// 기존 복사본에 source_screen_id 업데이트 (마이그레이션)
|
||||
await client.query(
|
||||
`UPDATE screen_definitions SET source_screen_id = $1 WHERE screen_id = $2`,
|
||||
[originalScreenId, existingCopyResult.rows[0].screen_id]
|
||||
[originalScreenId, existingCopy.screen_id]
|
||||
);
|
||||
logger.info(
|
||||
` 📝 기존 화면에 source_screen_id 추가: ${existingCopyResult.rows[0].screen_id} ← ${originalScreenId}`
|
||||
` 📝 기존 화면에 source_screen_id 추가: ${existingCopy.screen_id} ← ${originalScreenId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1375,10 +1422,9 @@ export class MenuCopyService {
|
||||
}
|
||||
}
|
||||
|
||||
if (existingCopyResult.rows.length > 0) {
|
||||
if (existingCopy) {
|
||||
// === 기존 복사본이 있는 경우: 업데이트 ===
|
||||
const existingScreen = existingCopyResult.rows[0];
|
||||
const existingScreenId = existingScreen.screen_id;
|
||||
const existingScreenId = existingCopy.screen_id;
|
||||
|
||||
// 원본 레이아웃 조회
|
||||
const sourceLayoutsResult = await client.query<ScreenLayout>(
|
||||
@@ -1526,36 +1572,39 @@ export class MenuCopyService {
|
||||
|
||||
// component_id 매핑 생성 (원본 → 새 ID)
|
||||
const componentIdMap = new Map<string, string>();
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const newComponentId = `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const timestamp = Date.now();
|
||||
layoutsResult.rows.forEach((layout, idx) => {
|
||||
const newComponentId = `comp_${timestamp}_${idx}_${Math.random().toString(36).substr(2, 5)}`;
|
||||
componentIdMap.set(layout.component_id, newComponentId);
|
||||
}
|
||||
});
|
||||
|
||||
// 레이아웃 삽입
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const newComponentId = componentIdMap.get(layout.component_id)!;
|
||||
// 레이아웃 배치 삽입 준비
|
||||
if (layoutsResult.rows.length > 0) {
|
||||
const layoutValues: string[] = [];
|
||||
const layoutParams: any[] = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
const newParentId = layout.parent_id
|
||||
? componentIdMap.get(layout.parent_id) || layout.parent_id
|
||||
: null;
|
||||
const newZoneId = layout.zone_id
|
||||
? componentIdMap.get(layout.zone_id) || layout.zone_id
|
||||
: null;
|
||||
for (const layout of layoutsResult.rows) {
|
||||
const newComponentId = componentIdMap.get(layout.component_id)!;
|
||||
|
||||
const updatedProperties = this.updateReferencesInProperties(
|
||||
layout.properties,
|
||||
screenIdMap,
|
||||
flowIdMap,
|
||||
numberingRuleIdMap
|
||||
);
|
||||
const newParentId = layout.parent_id
|
||||
? componentIdMap.get(layout.parent_id) || layout.parent_id
|
||||
: null;
|
||||
const newZoneId = layout.zone_id
|
||||
? componentIdMap.get(layout.zone_id) || layout.zone_id
|
||||
: null;
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts (
|
||||
screen_id, component_type, component_id, parent_id,
|
||||
position_x, position_y, width, height, properties,
|
||||
display_order, layout_type, layout_config, zones_config, zone_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`,
|
||||
[
|
||||
const updatedProperties = this.updateReferencesInProperties(
|
||||
layout.properties,
|
||||
screenIdMap,
|
||||
flowIdMap,
|
||||
numberingRuleIdMap
|
||||
);
|
||||
|
||||
layoutValues.push(
|
||||
`($${paramIdx}, $${paramIdx + 1}, $${paramIdx + 2}, $${paramIdx + 3}, $${paramIdx + 4}, $${paramIdx + 5}, $${paramIdx + 6}, $${paramIdx + 7}, $${paramIdx + 8}, $${paramIdx + 9}, $${paramIdx + 10}, $${paramIdx + 11}, $${paramIdx + 12}, $${paramIdx + 13})`
|
||||
);
|
||||
layoutParams.push(
|
||||
targetScreenId,
|
||||
layout.component_type,
|
||||
newComponentId,
|
||||
@@ -1569,8 +1618,19 @@ export class MenuCopyService {
|
||||
layout.layout_type,
|
||||
layout.layout_config,
|
||||
layout.zones_config,
|
||||
newZoneId,
|
||||
]
|
||||
newZoneId
|
||||
);
|
||||
paramIdx += 14;
|
||||
}
|
||||
|
||||
// 배치 INSERT
|
||||
await client.query(
|
||||
`INSERT INTO screen_layouts (
|
||||
screen_id, component_type, component_id, parent_id,
|
||||
position_x, position_y, width, height, properties,
|
||||
display_order, layout_type, layout_config, zones_config, zone_id
|
||||
) VALUES ${layoutValues.join(", ")}`,
|
||||
layoutParams
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2185,6 +2245,101 @@ export class MenuCopyService {
|
||||
return { copiedCategories, copiedCodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* 화면에서 참조하는 채번규칙 매핑 보완
|
||||
* 화면 properties에서 참조하는 채번규칙 중 아직 매핑되지 않은 것들을
|
||||
* 대상 회사에서 같은 이름(rule_name)의 채번규칙으로 매핑
|
||||
*/
|
||||
private async supplementNumberingRuleMapping(
|
||||
screenIds: number[],
|
||||
sourceCompanyCode: string,
|
||||
targetCompanyCode: string,
|
||||
numberingRuleIdMap: Map<string, string>,
|
||||
client: PoolClient
|
||||
): Promise<void> {
|
||||
if (screenIds.length === 0) return;
|
||||
|
||||
// 1. 화면 레이아웃에서 모든 채번규칙 ID 추출
|
||||
const layoutsResult = await client.query(
|
||||
`SELECT properties::text as props FROM screen_layouts WHERE screen_id = ANY($1)`,
|
||||
[screenIds]
|
||||
);
|
||||
|
||||
const referencedRuleIds = new Set<string>();
|
||||
const ruleIdRegex = /"numberingRuleId"\s*:\s*"([^"]+)"/g;
|
||||
|
||||
for (const row of layoutsResult.rows) {
|
||||
if (!row.props) continue;
|
||||
let match;
|
||||
while ((match = ruleIdRegex.exec(row.props)) !== null) {
|
||||
const ruleId = match[1];
|
||||
// 이미 매핑된 것은 제외
|
||||
if (ruleId && !numberingRuleIdMap.has(ruleId)) {
|
||||
referencedRuleIds.add(ruleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (referencedRuleIds.size === 0) {
|
||||
logger.info(` 📭 추가 매핑 필요 없음`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(` 🔍 매핑 필요한 채번규칙: ${referencedRuleIds.size}개`);
|
||||
|
||||
// 2. 원본 채번규칙 정보 조회 (rule_name으로 대상 회사에서 찾기 위해)
|
||||
const sourceRulesResult = await client.query(
|
||||
`SELECT rule_id, rule_name, table_name FROM numbering_rules
|
||||
WHERE rule_id = ANY($1)`,
|
||||
[Array.from(referencedRuleIds)]
|
||||
);
|
||||
|
||||
if (sourceRulesResult.rows.length === 0) {
|
||||
logger.warn(
|
||||
` ⚠️ 원본 채번규칙 조회 실패: ${Array.from(referencedRuleIds).join(", ")}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 대상 회사에서 같은 이름의 채번규칙 찾기
|
||||
const ruleNames = sourceRulesResult.rows.map((r) => r.rule_name);
|
||||
const targetRulesResult = await client.query(
|
||||
`SELECT rule_id, rule_name, table_name FROM numbering_rules
|
||||
WHERE rule_name = ANY($1) AND company_code = $2`,
|
||||
[ruleNames, targetCompanyCode]
|
||||
);
|
||||
|
||||
// rule_name -> target_rule_id 매핑
|
||||
const targetRulesByName = new Map<string, string>();
|
||||
for (const r of targetRulesResult.rows) {
|
||||
// 같은 이름이 여러 개일 수 있으므로 첫 번째만 사용
|
||||
if (!targetRulesByName.has(r.rule_name)) {
|
||||
targetRulesByName.set(r.rule_name, r.rule_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 매핑 추가
|
||||
let mappedCount = 0;
|
||||
for (const sourceRule of sourceRulesResult.rows) {
|
||||
const targetRuleId = targetRulesByName.get(sourceRule.rule_name);
|
||||
if (targetRuleId) {
|
||||
numberingRuleIdMap.set(sourceRule.rule_id, targetRuleId);
|
||||
logger.info(
|
||||
` ✅ 채번규칙 매핑 추가: ${sourceRule.rule_id} (${sourceRule.rule_name}) → ${targetRuleId}`
|
||||
);
|
||||
mappedCount++;
|
||||
} else {
|
||||
logger.warn(
|
||||
` ⚠️ 대상 회사에 같은 이름의 채번규칙 없음: ${sourceRule.rule_name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
` ✅ 채번규칙 매핑 보완 완료: ${mappedCount}/${referencedRuleIds.size}개`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 채번 규칙 복사 (최적화: 배치 조회/삽입)
|
||||
* 화면 복사 전에 호출되어 numberingRuleId 참조 업데이트에 사용됨
|
||||
@@ -2215,10 +2370,12 @@ export class MenuCopyService {
|
||||
return { copiedCount, ruleIdMap };
|
||||
}
|
||||
|
||||
// 2. 대상 회사에 이미 존재하는 모든 채번 규칙 조회 (원본 ID + 새로 생성될 ID 모두 체크)
|
||||
// 2. 대상 회사에 이미 존재하는 채번 규칙 한 번에 조회
|
||||
const ruleIds = allRulesResult.rows.map((r) => r.rule_id);
|
||||
const existingRulesResult = await client.query(
|
||||
`SELECT rule_id FROM numbering_rules WHERE company_code = $1`,
|
||||
[targetCompanyCode]
|
||||
`SELECT rule_id FROM numbering_rules
|
||||
WHERE rule_id = ANY($1) AND company_code = $2`,
|
||||
[ruleIds, targetCompanyCode]
|
||||
);
|
||||
const existingRuleIds = new Set(
|
||||
existingRulesResult.rows.map((r) => r.rule_id)
|
||||
@@ -2228,21 +2385,29 @@ export class MenuCopyService {
|
||||
const rulesToCopy: any[] = [];
|
||||
const originalToNewRuleMap: Array<{ original: string; new: string }> = [];
|
||||
|
||||
for (const rule of allRulesResult.rows) {
|
||||
// 새 rule_id 생성
|
||||
const originalSuffix = rule.rule_id.includes("_")
|
||||
? rule.rule_id.replace(/^[^_]*_/, "")
|
||||
: rule.rule_id;
|
||||
const newRuleId = `${targetCompanyCode}_${originalSuffix}`;
|
||||
// 기존 규칙 중 menu_objid 업데이트가 필요한 규칙들
|
||||
const rulesToUpdate: Array<{ ruleId: string; newMenuObjid: number }> = [];
|
||||
|
||||
// 원본 ID 또는 새로 생성될 ID가 이미 존재하는 경우 스킵
|
||||
for (const rule of allRulesResult.rows) {
|
||||
if (existingRuleIds.has(rule.rule_id)) {
|
||||
// 기존 규칙은 동일한 ID로 매핑
|
||||
ruleIdMap.set(rule.rule_id, rule.rule_id);
|
||||
logger.info(` ♻️ 채번규칙 이미 존재 (원본 ID): ${rule.rule_id}`);
|
||||
} else if (existingRuleIds.has(newRuleId)) {
|
||||
ruleIdMap.set(rule.rule_id, newRuleId);
|
||||
logger.info(` ♻️ 채번규칙 이미 존재 (대상 ID): ${newRuleId}`);
|
||||
|
||||
// 새 메뉴 ID로 연결 업데이트 필요
|
||||
const newMenuObjid = menuIdMap.get(rule.menu_objid);
|
||||
if (newMenuObjid) {
|
||||
rulesToUpdate.push({ ruleId: rule.rule_id, newMenuObjid });
|
||||
}
|
||||
logger.info(
|
||||
` ♻️ 채번규칙 이미 존재 (메뉴 연결 갱신): ${rule.rule_id}`
|
||||
);
|
||||
} else {
|
||||
// 새 rule_id 생성
|
||||
const originalSuffix = rule.rule_id.includes("_")
|
||||
? rule.rule_id.replace(/^[^_]*_/, "")
|
||||
: rule.rule_id;
|
||||
const newRuleId = `${targetCompanyCode}_${originalSuffix}`;
|
||||
|
||||
ruleIdMap.set(rule.rule_id, newRuleId);
|
||||
originalToNewRuleMap.push({ original: rule.rule_id, new: newRuleId });
|
||||
rulesToCopy.push({ ...rule, newRuleId });
|
||||
@@ -2288,8 +2453,30 @@ export class MenuCopyService {
|
||||
|
||||
copiedCount = rulesToCopy.length;
|
||||
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사`);
|
||||
}
|
||||
|
||||
// 5. 모든 원본 파트 한 번에 조회
|
||||
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
|
||||
if (rulesToUpdate.length > 0) {
|
||||
// CASE WHEN을 사용한 배치 업데이트
|
||||
const caseWhen = rulesToUpdate
|
||||
.map((_, i) => `WHEN rule_id = $${i * 2 + 1} THEN $${i * 2 + 2}`)
|
||||
.join(" ");
|
||||
const ruleIdsForUpdate = rulesToUpdate.map((r) => r.ruleId);
|
||||
const params = rulesToUpdate.flatMap((r) => [r.ruleId, r.newMenuObjid]);
|
||||
|
||||
await client.query(
|
||||
`UPDATE numbering_rules
|
||||
SET menu_objid = CASE ${caseWhen} END, updated_at = NOW()
|
||||
WHERE rule_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`,
|
||||
[...params, ruleIdsForUpdate, targetCompanyCode]
|
||||
);
|
||||
logger.info(
|
||||
` ✅ 기존 채번 규칙 ${rulesToUpdate.length}개 메뉴 연결 갱신`
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 모든 원본 파트 한 번에 조회 (새로 복사한 규칙만 대상)
|
||||
if (rulesToCopy.length > 0) {
|
||||
const originalRuleIds = rulesToCopy.map((r) => r.rule_id);
|
||||
const allPartsResult = await client.query(
|
||||
`SELECT * FROM numbering_rule_parts
|
||||
@@ -2380,11 +2567,24 @@ export class MenuCopyService {
|
||||
])
|
||||
);
|
||||
|
||||
// 3. 복사할 매핑 필터링 및 배치 INSERT
|
||||
const mappingsToCopy = allMappingsResult.rows.filter(
|
||||
(m) =>
|
||||
!existingMappingKeys.has(`${m.table_name}|${m.logical_column_name}`)
|
||||
);
|
||||
// 3. 복사할 매핑 필터링 및 기존 매핑 업데이트 대상 분류
|
||||
const mappingsToCopy: any[] = [];
|
||||
const mappingsToUpdate: Array<{ mappingId: number; newMenuObjid: number }> =
|
||||
[];
|
||||
|
||||
for (const m of allMappingsResult.rows) {
|
||||
const key = `${m.table_name}|${m.logical_column_name}`;
|
||||
if (existingMappingKeys.has(key)) {
|
||||
// 기존 매핑은 menu_objid만 업데이트
|
||||
const existingMappingId = existingMappingKeys.get(key);
|
||||
const newMenuObjid = menuIdMap.get(m.menu_objid);
|
||||
if (existingMappingId && newMenuObjid) {
|
||||
mappingsToUpdate.push({ mappingId: existingMappingId, newMenuObjid });
|
||||
}
|
||||
} else {
|
||||
mappingsToCopy.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
// 새 매핑 ID -> 원본 매핑 정보 추적
|
||||
const mappingInsertInfo: Array<{ mapping: any; newMenuObjid: number }> = [];
|
||||
@@ -2433,6 +2633,29 @@ export class MenuCopyService {
|
||||
logger.info(` ✅ 카테고리 매핑 ${copiedCount}개 복사`);
|
||||
}
|
||||
|
||||
// 3-1. 기존 카테고리 매핑의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
|
||||
if (mappingsToUpdate.length > 0) {
|
||||
// CASE WHEN을 사용한 배치 업데이트
|
||||
const caseWhen = mappingsToUpdate
|
||||
.map((_, i) => `WHEN mapping_id = $${i * 2 + 1} THEN $${i * 2 + 2}`)
|
||||
.join(" ");
|
||||
const mappingIdsForUpdate = mappingsToUpdate.map((m) => m.mappingId);
|
||||
const params = mappingsToUpdate.flatMap((m) => [
|
||||
m.mappingId,
|
||||
m.newMenuObjid,
|
||||
]);
|
||||
|
||||
await client.query(
|
||||
`UPDATE category_column_mapping
|
||||
SET menu_objid = CASE ${caseWhen} END
|
||||
WHERE mapping_id = ANY($${params.length + 1}) AND company_code = $${params.length + 2}`,
|
||||
[...params, mappingIdsForUpdate, targetCompanyCode]
|
||||
);
|
||||
logger.info(
|
||||
` ✅ 기존 카테고리 매핑 ${mappingsToUpdate.length}개 메뉴 연결 갱신`
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 모든 원본 카테고리 값 한 번에 조회
|
||||
const allValuesResult = await client.query(
|
||||
`SELECT * FROM table_column_category_values
|
||||
|
||||
Reference in New Issue
Block a user