import { logger } from "../utils/logger"; import { query, queryOne } from "../database/db"; export class AdminService { /** * 관리자 메뉴 목록 조회 (회사별 필터링 적용) */ static async getAdminMenuList(paramMap: any): Promise { try { logger.info("AdminService.getAdminMenuList 시작 - 파라미터:", paramMap); const { userId, userCompanyCode, userType, userLang = "ko", menuType, } = paramMap; // menuType에 따른 WHERE 조건 생성 const menuTypeCondition = menuType !== undefined ? `MENU_TYPE = ${parseInt(menuType)}` : "1 = 1"; // 1. 권한 그룹 기반 필터링 (좌측 사이드바인 경우만) let authFilter = ""; let queryParams: any[] = [userLang]; let paramIndex = 2; if (menuType !== undefined && userType !== "SUPER_ADMIN") { // 좌측 사이드바 + SUPER_ADMIN이 아닌 경우 const userRoleGroups = await query( ` SELECT DISTINCT am.objid AS role_objid, am.auth_name FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = $1 AND am.status = 'active' `, [userId] ); logger.info( `✅ 사용자 ${userId}가 속한 권한 그룹: ${userRoleGroups.length}개`, { roleGroups: userRoleGroups.map((rg: any) => rg.auth_name), } ); if (userType === "COMPANY_ADMIN") { // 회사 관리자: 자기 회사 메뉴는 모두, 공통 메뉴는 권한 있는 것만 if (userRoleGroups.length > 0) { const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid); // 루트 메뉴: 회사 코드만 체크 (권한 체크 X) // 하위 메뉴: 회사 메뉴는 모두, 공통 메뉴는 권한 체크 authFilter = `AND MENU.COMPANY_CODE IN ($${paramIndex}, '*')`; queryParams.push(userCompanyCode); queryParams.push(roleObjids); paramIndex += 2; logger.info( `✅ 회사 관리자: 회사 ${userCompanyCode} 메뉴 전체 + 권한 있는 공통 메뉴` ); } else { // 권한 그룹이 없는 회사 관리자: 자기 회사 메뉴만 authFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; queryParams.push(userCompanyCode); paramIndex++; logger.info( `✅ 회사 관리자 (권한 그룹 없음): 회사 ${userCompanyCode} 메뉴만` ); } } else { // 일반 사용자: 권한 그룹 필수 if (userRoleGroups.length > 0) { const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid); authFilter = ` AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU.OBJID AND rma.auth_objid = ANY($${paramIndex}) AND rma.read_yn = 'Y' ) `; queryParams.push(roleObjids); paramIndex++; logger.info( `✅ 일반 사용자: 권한 있는 메뉴만 (${roleObjids.length}개 그룹)` ); } else { // 권한 그룹이 없는 일반 사용자: 메뉴 없음 logger.warn( `⚠️ 사용자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.` ); return []; } } } else if (menuType !== undefined && userType === "SUPER_ADMIN") { // 좌측 사이드바 + SUPER_ADMIN: 권한 그룹 체크 없이 모든 공통 메뉴 표시 logger.info(`✅ 최고 관리자는 권한 그룹 체크 없이 모든 공통 메뉴 표시`); } // 2. 회사별 필터링 조건 생성 let companyFilter = ""; // SUPER_ADMIN과 COMPANY_ADMIN 구분 if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { // SUPER_ADMIN if (menuType === undefined) { // 메뉴 관리 화면: 모든 메뉴 logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시"); companyFilter = ""; } else { // 좌측 사이드바: 공통 메뉴만 (company_code = '*') logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시"); companyFilter = `AND MENU.COMPANY_CODE = '*'`; } } else if (menuType === undefined) { // 메뉴 관리 화면: 자기 회사 + 공통 메뉴 logger.info( `✅ 메뉴 관리 화면: 회사 ${userCompanyCode} + 공통 메뉴 표시` ); companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE = '*')`; queryParams.push(userCompanyCode); paramIndex++; } // menuType이 정의된 경우 (좌측 사이드바)는 authFilter에서 이미 회사 필터링 포함 // 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅 // WITH RECURSIVE 쿼리 구현 const menuList = await query( ` WITH RECURSIVE v_menu( LEVEL, MENU_TYPE, OBJID, PARENT_OBJ_ID, MENU_NAME_KOR, MENU_URL, MENU_DESC, SEQ, WRITER, REGDATE, STATUS, SYSTEM_NAME, COMPANY_CODE, LANG_KEY, LANG_KEY_DESC, PATH, CYCLE, TRANSLATED_NAME, TRANSLATED_DESC ) AS ( SELECT 1 AS LEVEL, MENU.MENU_TYPE, MENU.OBJID::numeric, MENU.PARENT_OBJ_ID, MENU.MENU_NAME_KOR, MENU.MENU_URL, MENU.MENU_DESC, MENU.SEQ, MENU.WRITER, MENU.REGDATE, MENU.STATUS, MENU.SYSTEM_NAME, MENU.COMPANY_CODE, MENU.LANG_KEY, MENU.LANG_KEY_DESC, ARRAY [MENU.OBJID], FALSE, -- 번역된 메뉴명 (우선순위: 회사별 번역 > 공통 번역 > 기본명) COALESCE( (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU.LANG_KEY AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*')) AND MLT.lang_code = $1 LIMIT 1), (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU.LANG_KEY AND MLKM.company_code = '*' AND MLT.lang_code = $1 LIMIT 1), MENU.MENU_NAME_KOR ), -- 번역된 설명 (우선순위: 회사별 번역 > 공통 번역 > 기본명) COALESCE( (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU.LANG_KEY_DESC AND (MLKM.company_code = MENU.COMPANY_CODE OR (MENU.COMPANY_CODE IS NULL AND MLKM.company_code = '*')) AND MLT.lang_code = $1 LIMIT 1), (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU.LANG_KEY_DESC AND MLKM.company_code = '*' AND MLT.lang_code = $1 LIMIT 1), MENU.MENU_DESC ) FROM MENU_INFO MENU WHERE ${menuTypeCondition} AND STATUS = 'active' ${companyFilter} ${authFilter} AND NOT EXISTS ( SELECT 1 FROM MENU_INFO parent_menu WHERE parent_menu.OBJID = MENU.PARENT_OBJ_ID ) UNION ALL SELECT V_MENU.LEVEL + 1, MENU_SUB.MENU_TYPE, MENU_SUB.OBJID, MENU_SUB.PARENT_OBJ_ID, MENU_SUB.MENU_NAME_KOR, MENU_SUB.MENU_URL, MENU_SUB.MENU_DESC, MENU_SUB.SEQ, MENU_SUB.WRITER, MENU_SUB.REGDATE, MENU_SUB.STATUS, MENU_SUB.SYSTEM_NAME, MENU_SUB.COMPANY_CODE, MENU_SUB.LANG_KEY, MENU_SUB.LANG_KEY_DESC, PATH || MENU_SUB.SEQ::numeric, MENU_SUB.OBJID = ANY(PATH), -- 번역된 메뉴명 (우선순위: 회사별 번역 > 공통 번역 > 기본명) COALESCE( (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU_SUB.LANG_KEY AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*')) AND MLT.lang_code = $1 LIMIT 1), (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU_SUB.LANG_KEY AND MLKM.company_code = '*' AND MLT.lang_code = $1 LIMIT 1), MENU_SUB.MENU_NAME_KOR ), -- 번역된 설명 (우선순위: 회사별 번역 > 공통 번역 > 기본명) COALESCE( (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC AND (MLKM.company_code = MENU_SUB.COMPANY_CODE OR (MENU_SUB.COMPANY_CODE IS NULL AND MLKM.company_code = '*')) AND MLT.lang_code = $1 LIMIT 1), (SELECT MLT.lang_text FROM MULTI_LANG_KEY_MASTER MLKM JOIN MULTI_LANG_TEXT MLT ON MLKM.key_id = MLT.key_id WHERE MLKM.lang_key = MENU_SUB.LANG_KEY_DESC AND MLKM.company_code = '*' AND MLT.lang_code = $1 LIMIT 1), MENU_SUB.MENU_DESC ) FROM MENU_INFO MENU_SUB JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH) AND MENU_SUB.STATUS = 'active' AND ( MENU_SUB.COMPANY_CODE = $2 OR ( MENU_SUB.COMPANY_CODE = '*' AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU_SUB.OBJID AND rma.auth_objid = ANY($3) AND rma.read_yn = 'Y' ) ) ) ) SELECT LEVEL AS LEV, CAST(MENU_TYPE AS TEXT) AS MENU_TYPE, A.OBJID, A.PARENT_OBJ_ID, A.MENU_NAME_KOR, LPAD(' ', 3 * (LEVEL - 1)) || A.MENU_NAME_KOR AS LPAD_MENU_NAME_KOR, A.MENU_URL, A.MENU_DESC, A.SEQ, A.WRITER, TO_CHAR(A.REGDATE, 'YYYY-MM-DD') AS REGDATE, A.STATUS, A.COMPANY_CODE, A.LANG_KEY, A.LANG_KEY_DESC, COALESCE(CM.COMPANY_NAME, '미지정') AS COMPANY_NAME, A.TRANSLATED_NAME, A.TRANSLATED_DESC, CASE UPPER(A.STATUS) WHEN 'ACTIVE' THEN '활성화' WHEN 'INACTIVE' THEN '비활성화' ELSE '' END AS STATUS_TITLE FROM v_menu A LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE WHERE 1 = 1 ORDER BY PATH, SEQ `, queryParams ); logger.info( `관리자 메뉴 목록 조회 결과: ${menuList.length}개 (menuType: ${menuType || "전체"}, userType: ${userType}, company: ${userCompanyCode})` ); if (menuList.length > 0) { logger.info("첫 번째 메뉴:", { objid: menuList[0].objid, name: menuList[0].menu_name_kor, companyCode: menuList[0].company_code, }); } return menuList; } catch (error) { logger.error("AdminService.getAdminMenuList 오류:", error); throw error; } } /** * 사용자 메뉴 목록 조회 (권한 그룹 기반 필터링) */ static async getUserMenuList(paramMap: any): Promise { try { logger.info("AdminService.getUserMenuList 시작 - 파라미터:", paramMap); const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap; // 1. 사용자가 속한 권한 그룹 조회 const userRoleGroups = await query( ` SELECT DISTINCT am.objid AS role_objid, am.auth_name FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = $1 AND am.status = 'active' `, [userId] ); logger.info( `✅ 사용자 ${userId}가 속한 권한 그룹: ${userRoleGroups.length}개`, { roleGroups: userRoleGroups.map((rg: any) => rg.auth_name), } ); // 2. 권한 그룹 기반 메뉴 필터 조건 생성 let authFilter = ""; let queryParams: any[] = [userLang]; let paramIndex = 2; if (userRoleGroups.length > 0) { // 권한 그룹이 있는 경우: read_yn = 'Y'인 메뉴만 필터링 const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid); authFilter = ` AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU.OBJID AND rma.auth_objid = ANY($${paramIndex}) AND rma.read_yn = 'Y' ) `; queryParams.push(roleObjids); paramIndex++; logger.info( `✅ 권한 그룹 기반 메뉴 필터링 적용: ${roleObjids.length}개 그룹` ); } else { // 권한 그룹이 없는 경우: 메뉴 없음 logger.warn( `⚠️ 사용자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.` ); return []; } // 3. 회사별 필터링 조건 생성 let companyFilter = ""; if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { // SUPER_ADMIN: 공통 메뉴만 (company_code = '*') logger.info("✅ 좌측 사이드바 (SUPER_ADMIN): 공통 메뉴만 표시"); companyFilter = `AND MENU.COMPANY_CODE = '*'`; } else { // COMPANY_ADMIN/USER: 자기 회사 메뉴만 logger.info( `✅ 좌측 사이드바 (COMPANY_ADMIN): 회사 ${userCompanyCode} 메뉴만 표시` ); companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; queryParams.push(userCompanyCode); paramIndex++; } // 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅 + 회사별 필터링 const menuList = await query( ` WITH RECURSIVE v_menu( LEVEL, MENU_TYPE, OBJID, PARENT_OBJ_ID, MENU_NAME_KOR, MENU_URL, MENU_DESC, SEQ, WRITER, REGDATE, STATUS, COMPANY_CODE, LANG_KEY, LANG_KEY_DESC, PATH, CYCLE ) AS ( SELECT 1 AS LEVEL, MENU_TYPE, OBJID::numeric, PARENT_OBJ_ID, MENU_NAME_KOR, MENU_URL, MENU_DESC, SEQ, WRITER, REGDATE, STATUS, COMPANY_CODE, LANG_KEY, LANG_KEY_DESC, ARRAY [MENU.OBJID], FALSE FROM MENU_INFO MENU WHERE PARENT_OBJ_ID = 0 AND MENU_TYPE = 1 AND STATUS = 'active' ${companyFilter} ${authFilter} UNION ALL SELECT V_MENU.LEVEL + 1, MENU_SUB.MENU_TYPE, MENU_SUB.OBJID, MENU_SUB.PARENT_OBJ_ID, MENU_SUB.MENU_NAME_KOR, MENU_SUB.MENU_URL, MENU_SUB.MENU_DESC, MENU_SUB.SEQ, MENU_SUB.WRITER, MENU_SUB.REGDATE, MENU_SUB.STATUS, MENU_SUB.COMPANY_CODE, MENU_SUB.LANG_KEY, MENU_SUB.LANG_KEY_DESC, PATH || MENU_SUB.SEQ::numeric, MENU_SUB.OBJID = ANY(PATH) FROM MENU_INFO MENU_SUB JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID WHERE MENU_SUB.STATUS = 'active' ${authFilter.replace(/MENU\.OBJID/g, "MENU_SUB.OBJID")} ) SELECT LEVEL AS LEV, CASE MENU_TYPE WHEN '0' THEN 'admin' WHEN '1' THEN 'user' ELSE '' END AS MENU_TYPE, A.OBJID, A.PARENT_OBJ_ID, A.MENU_NAME_KOR, LPAD(' ', 3 * (LEVEL - 1)) || A.MENU_NAME_KOR AS LPAD_MENU_NAME_KOR, A.MENU_URL, A.MENU_DESC, A.SEQ, A.WRITER, TO_CHAR(A.REGDATE, 'YYYY-MM-DD') AS REGDATE, A.STATUS, A.COMPANY_CODE, A.LANG_KEY, A.LANG_KEY_DESC, COALESCE(CM.COMPANY_NAME, '미지정') AS COMPANY_NAME, -- 번역된 메뉴명 (우선순위: 번역 > 기본명) COALESCE(MLT_NAME.lang_text, A.MENU_NAME_KOR) AS TRANSLATED_NAME, -- 번역된 설명 (우선순위: 번역 > 기본명) COALESCE(MLT_DESC.lang_text, A.MENU_DESC) AS TRANSLATED_DESC, CASE UPPER(A.STATUS) WHEN 'ACTIVE' THEN '활성화' WHEN 'INACTIVE' THEN '비활성화' ELSE '' END AS STATUS_TITLE FROM v_menu A LEFT JOIN COMPANY_MNG CM ON A.COMPANY_CODE = CM.COMPANY_CODE LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_NAME ON A.LANG_KEY = MLKM_NAME.lang_key LEFT JOIN MULTI_LANG_TEXT MLT_NAME ON MLKM_NAME.key_id = MLT_NAME.key_id AND MLT_NAME.lang_code = $1 LEFT JOIN MULTI_LANG_KEY_MASTER MLKM_DESC ON A.LANG_KEY_DESC = MLKM_DESC.lang_key LEFT JOIN MULTI_LANG_TEXT MLT_DESC ON MLKM_DESC.key_id = MLT_DESC.key_id AND MLT_DESC.lang_code = $1 WHERE 1 = 1 ORDER BY PATH, SEQ `, queryParams ); logger.info( `사용자 메뉴 목록 조회 결과: ${menuList.length}개 (userType: ${userType}, company: ${userCompanyCode})` ); if (menuList.length > 0) { logger.info("첫 번째 메뉴:", { objid: menuList[0].objid, name: menuList[0].menu_name_kor, companyCode: menuList[0].company_code, }); } return menuList; } catch (error) { logger.error("AdminService.getUserMenuList 오류:", error); throw error; } } /** * 메뉴 정보 조회 */ static async getMenuInfo(menuId: string): Promise { try { logger.info(`AdminService.getMenuInfo 시작 - menuId: ${menuId}`); // Raw Query를 사용한 메뉴 정보 조회 (회사 정보 포함) const menuResult = await query( `SELECT m.*, c.company_name FROM menu_info m LEFT JOIN company_mng c ON m.company_code = c.company_code WHERE m.objid = $1::numeric`, [menuId] ); if (!menuResult || menuResult.length === 0) { return null; } const menuInfo = menuResult[0]; // 응답 형식 조정 (기존 형식과 호환성 유지) const result = { ...menuInfo, objid: menuInfo.objid?.toString(), menu_type: menuInfo.menu_type?.toString(), parent_obj_id: menuInfo.parent_obj_id?.toString(), seq: menuInfo.seq?.toString(), company_name: menuInfo.company_name || "미지정", }; logger.info("메뉴 정보 조회 결과:", result); return result; } catch (error) { logger.error("AdminService.getMenuInfo 오류:", error); throw error; } } }