feat: 메뉴 복사 기능 - 2단계 복사 방식으로 화면 참조 매핑 문제 해결

- 문제: 화면 복사 시 참조되는 화면이 아직 복사되지 않아 screenIdMap에 매핑 정보가 없었음
- 해결: 2단계 복사 방식 도입
  1단계: 모든 screen_definitions 먼저 복사하여 screenIdMap 완성
  2단계: screen_layouts 복사하면서 완성된 screenIdMap으로 참조 업데이트
- 결과: targetScreenId가 올바르게 새 회사의 화면 ID로 매핑됨 (예: 149 → 517)
- 추가: 화면 수집 시 문자열 타입 ID도 올바르게 파싱하도록 개선
- 추가: 참조 화면 발견 및 업데이트 로그 추가

관련 파일:
- backend-node/src/services/menuCopyService.ts
- db/migrations/1003_add_source_menu_objid_to_menu_info.sql
- db/scripts/cleanup_company_11_*.sql
This commit is contained in:
kjs
2025-11-21 14:37:09 +09:00
parent bb49073bf7
commit c70998fa4f
11 changed files with 4021 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ import { AdminService } from "../services/adminService";
import { EncryptUtil } from "../utils/encryptUtil";
import { FileSystemManager } from "../utils/fileSystemManager";
import { validateBusinessNumber } from "../utils/businessNumberValidator";
import { MenuCopyService } from "../services/menuCopyService";
/**
* 관리자 메뉴 목록 조회
@@ -3253,3 +3254,84 @@ export async function getTableSchema(
});
}
}
/**
* 메뉴 복사
* POST /api/admin/menus/:menuObjid/copy
*/
export async function copyMenu(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { menuObjid } = req.params;
const { targetCompanyCode } = req.body;
const userId = req.user!.userId;
const userCompanyCode = req.user!.companyCode;
const userType = req.user!.userType;
const isSuperAdmin = req.user!.isSuperAdmin;
logger.info(`
=== 메뉴 복사 API 호출 ===
menuObjid: ${menuObjid}
targetCompanyCode: ${targetCompanyCode}
userId: ${userId}
userCompanyCode: ${userCompanyCode}
userType: ${userType}
isSuperAdmin: ${isSuperAdmin}
`);
// 권한 체크: 최고 관리자만 가능
if (!isSuperAdmin && userType !== "SUPER_ADMIN") {
logger.warn(`권한 없음: ${userId} (userType=${userType}, company_code=${userCompanyCode})`);
res.status(403).json({
success: false,
message: "메뉴 복사는 최고 관리자(SUPER_ADMIN)만 가능합니다",
error: {
code: "FORBIDDEN",
details: "Only super admin can copy menus",
},
});
return;
}
// 필수 파라미터 검증
if (!menuObjid || !targetCompanyCode) {
res.status(400).json({
success: false,
message: "필수 파라미터가 누락되었습니다",
error: {
code: "MISSING_PARAMETERS",
details: "menuObjid and targetCompanyCode are required",
},
});
return;
}
// 메뉴 복사 실행
const menuCopyService = new MenuCopyService();
const result = await menuCopyService.copyMenu(
parseInt(menuObjid, 10),
targetCompanyCode,
userId
);
logger.info("✅ 메뉴 복사 API 성공");
res.json({
success: true,
message: "메뉴 복사 완료",
data: result,
});
} catch (error: any) {
logger.error("❌ 메뉴 복사 API 실패:", error);
res.status(500).json({
success: false,
message: "메뉴 복사 중 오류가 발생했습니다",
error: {
code: "MENU_COPY_ERROR",
details: error.message || "Unknown error",
},
});
}
}

View File

@@ -8,6 +8,7 @@ import {
deleteMenu, // 메뉴 삭제
deleteMenusBatch, // 메뉴 일괄 삭제
toggleMenuStatus, // 메뉴 상태 토글
copyMenu, // 메뉴 복사
getUserList,
getUserInfo, // 사용자 상세 조회
getUserHistory, // 사용자 변경이력 조회
@@ -39,6 +40,7 @@ router.get("/menus", getAdminMenus);
router.get("/user-menus", getUserMenus);
router.get("/menus/:menuId", getMenuInfo);
router.post("/menus", saveMenu); // 메뉴 추가
router.post("/menus/:menuObjid/copy", copyMenu); // 메뉴 복사 (NEW!)
router.put("/menus/:menuId", updateMenu); // 메뉴 수정
router.put("/menus/:menuId/toggle", toggleMenuStatus); // 메뉴 상태 토글
router.delete("/menus/batch", deleteMenusBatch); // 메뉴 일괄 삭제 (순서 중요!)

File diff suppressed because it is too large Load Diff