import { query } from "../database/db"; import { logger } from "../utils/logger"; /** * 권한 그룹 인터페이스 */ export interface RoleGroup { objid: number; authName: string; authCode: string; companyCode: string; status: string; writer: string; regdate: Date; memberCount?: number; menuCount?: number; memberNames?: string; } /** * 권한 그룹 멤버 인터페이스 */ export interface RoleMember { objid: number; masterObjid: number; userId: string; userName?: string; deptName?: string; positionName?: string; writer: string; regdate: Date; } /** * 메뉴 권한 인터페이스 */ export interface MenuPermission { objid: number; menuObjid: number; authObjid: number; menuName?: string; createYn: string; readYn: string; updateYn: string; deleteYn: string; writer: string; regdate: Date; } /** * 권한 그룹 서비스 */ export class RoleService { /** * 회사별 권한 그룹 목록 조회 * @param companyCode - 회사 코드 (undefined 시 전체 조회) * @param search - 검색어 */ static async getRoleGroups( companyCode?: string, search?: string ): Promise { try { let sql = ` SELECT objid, auth_name AS "authName", auth_code AS "authCode", company_code AS "companyCode", status, writer, regdate, (SELECT COUNT(*) FROM authority_sub_user asu WHERE asu.master_objid = am.objid) AS "memberCount", (SELECT COUNT(*) FROM rel_menu_auth rma WHERE rma.auth_objid = am.objid) AS "menuCount", (SELECT STRING_AGG(ui.user_name, ', ' ORDER BY ui.user_name) FROM authority_sub_user asu JOIN user_info ui ON asu.user_id = ui.user_id WHERE asu.master_objid = am.objid) AS "memberNames" FROM authority_master am WHERE 1=1 `; const params: any[] = []; let paramIndex = 1; // 회사 코드 필터 (companyCode가 undefined면 전체 조회) if (companyCode) { sql += ` AND company_code = $${paramIndex}`; params.push(companyCode); paramIndex++; } // 검색어 필터 if (search && search.trim()) { sql += ` AND (auth_name ILIKE $${paramIndex} OR auth_code ILIKE $${paramIndex})`; params.push(`%${search.trim()}%`); paramIndex++; } sql += ` ORDER BY regdate DESC`; logger.info("권한 그룹 조회 SQL", { sql, params }); const result = await query(sql, params); logger.info("권한 그룹 조회 결과", { count: result.length }); return result; } catch (error) { logger.error("권한 그룹 목록 조회 실패", { error, companyCode, search }); throw error; } } /** * 권한 그룹 상세 조회 */ static async getRoleGroupById(objid: number): Promise { try { const sql = ` SELECT objid, auth_name AS "authName", auth_code AS "authCode", company_code AS "companyCode", status, writer, regdate FROM authority_master WHERE objid = $1 `; const result = await query(sql, [objid]); return result.length > 0 ? result[0] : null; } catch (error) { logger.error("권한 그룹 상세 조회 실패", { error, objid }); throw error; } } /** * 권한 그룹 생성 */ static async createRoleGroup(data: { authName: string; authCode: string; companyCode: string; writer: string; }): Promise { try { const sql = ` INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate) VALUES (nextval('seq_authority_master'), $1, $2, $3, 'active', $4, NOW()) RETURNING objid, auth_name AS "authName", auth_code AS "authCode", company_code AS "companyCode", status, writer, regdate `; const result = await query(sql, [ data.authName, data.authCode, data.companyCode, data.writer, ]); logger.info("권한 그룹 생성 성공", { objid: result[0].objid, authName: data.authName, }); return result[0]; } catch (error) { logger.error("권한 그룹 생성 실패", { error, data }); throw error; } } /** * 권한 그룹 수정 */ static async updateRoleGroup( objid: number, data: { authName?: string; authCode?: string; status?: string; } ): Promise { try { const updates: string[] = []; const params: any[] = []; let paramIndex = 1; if (data.authName !== undefined) { updates.push(`auth_name = $${paramIndex}`); params.push(data.authName); paramIndex++; } if (data.authCode !== undefined) { updates.push(`auth_code = $${paramIndex}`); params.push(data.authCode); paramIndex++; } if (data.status !== undefined) { updates.push(`status = $${paramIndex}`); params.push(data.status); paramIndex++; } if (updates.length === 0) { throw new Error("수정할 데이터가 없습니다"); } params.push(objid); const sql = ` UPDATE authority_master SET ${updates.join(", ")} WHERE objid = $${paramIndex} RETURNING objid, auth_name AS "authName", auth_code AS "authCode", company_code AS "companyCode", status, writer, regdate `; const result = await query(sql, params); if (result.length === 0) { throw new Error("권한 그룹을 찾을 수 없습니다"); } logger.info("권한 그룹 수정 성공", { objid, updates }); return result[0]; } catch (error) { logger.error("권한 그룹 수정 실패", { error, objid, data }); throw error; } } /** * 권한 그룹 삭제 */ static async deleteRoleGroup(objid: number): Promise { try { // CASCADE로 연결된 데이터도 함께 삭제됨 (authority_sub_user, rel_menu_auth) await query("DELETE FROM authority_master WHERE objid = $1", [objid]); logger.info("권한 그룹 삭제 성공", { objid }); } catch (error) { logger.error("권한 그룹 삭제 실패", { error, objid }); throw error; } } /** * 권한 그룹 멤버 목록 조회 */ static async getRoleMembers(masterObjid: number): Promise { try { const sql = ` SELECT asu.objid, asu.master_objid AS "masterObjid", asu.user_id AS "userId", ui.user_name AS "userName", ui.dept_name AS "deptName", ui.position_name AS "positionName", asu.writer, asu.regdate FROM authority_sub_user asu JOIN user_info ui ON asu.user_id = ui.user_id WHERE asu.master_objid = $1 ORDER BY ui.user_name `; const result = await query(sql, [masterObjid]); return result; } catch (error) { logger.error("권한 그룹 멤버 조회 실패", { error, masterObjid }); throw error; } } /** * 권한 그룹 멤버 추가 (여러 명) */ static async addRoleMembers( masterObjid: number, userIds: string[], writer: string ): Promise { try { // 이미 존재하는 멤버 제외 const existingSql = ` SELECT user_id FROM authority_sub_user WHERE master_objid = $1 AND user_id = ANY($2) `; const existing = await query<{ user_id: string }>(existingSql, [ masterObjid, userIds, ]); const existingIds = new Set( existing.map((row: { user_id: string }) => row.user_id) ); const newUserIds = userIds.filter((userId) => !existingIds.has(userId)); if (newUserIds.length === 0) { logger.info("추가할 신규 멤버가 없습니다", { masterObjid, userIds }); return; } // 배치 삽입 const values = newUserIds .map( (_, index) => `(nextval('seq_authority_sub_user'), $1, $${index + 2}, $${newUserIds.length + 2}, NOW())` ) .join(", "); const sql = ` INSERT INTO authority_sub_user (objid, master_objid, user_id, writer, regdate) VALUES ${values} `; await query(sql, [masterObjid, ...newUserIds, writer]); // 히스토리 기록 for (const userId of newUserIds) { await this.insertAuthorityHistory(masterObjid, userId, "ADD", writer); } logger.info("권한 그룹 멤버 추가 성공", { masterObjid, count: newUserIds.length, }); } catch (error) { logger.error("권한 그룹 멤버 추가 실패", { error, masterObjid, userIds }); throw error; } } /** * 권한 그룹 멤버 제거 (여러 명) */ static async removeRoleMembers( masterObjid: number, userIds: string[], writer: string ): Promise { try { await query( "DELETE FROM authority_sub_user WHERE master_objid = $1 AND user_id = ANY($2)", [masterObjid, userIds] ); // 히스토리 기록 for (const userId of userIds) { await this.insertAuthorityHistory( masterObjid, userId, "REMOVE", writer ); } logger.info("권한 그룹 멤버 제거 성공", { masterObjid, count: userIds.length, }); } catch (error) { logger.error("권한 그룹 멤버 제거 실패", { error, masterObjid, userIds }); throw error; } } /** * 권한 히스토리 기록 */ private static async insertAuthorityHistory( masterObjid: number, userId: string, historyType: "ADD" | "REMOVE", writer: string ): Promise { try { const sql = ` INSERT INTO authority_master_history (objid, parent_objid, parent_name, parent_code, user_id, active, history_type, writer, reg_date) SELECT nextval('seq_authority_master'), $1, am.auth_name, am.auth_code, $2, am.status, $3, $4, NOW() FROM authority_master am WHERE am.objid = $1 `; await query(sql, [masterObjid, userId, historyType, writer]); } catch (error) { logger.error("권한 히스토리 기록 실패", { error, masterObjid, userId, historyType, }); // 히스토리 기록 실패는 메인 작업을 중단하지 않음 } } /** * 메뉴 권한 목록 조회 */ static async getMenuPermissions( authObjid: number ): Promise { try { const sql = ` SELECT rma.objid, rma.menu_objid AS "menuObjid", rma.auth_objid AS "authObjid", mi.menu_name_kor AS "menuName", mi.menu_code AS "menuCode", mi.menu_url AS "menuUrl", rma.create_yn AS "createYn", rma.read_yn AS "readYn", rma.update_yn AS "updateYn", rma.delete_yn AS "deleteYn", rma.execute_yn AS "executeYn", rma.export_yn AS "exportYn", rma.writer, rma.regdate FROM rel_menu_auth rma LEFT JOIN menu_info mi ON rma.menu_objid = mi.objid WHERE rma.auth_objid = $1 ORDER BY mi.menu_name_kor `; const result = await query(sql, [authObjid]); return result; } catch (error) { logger.error("메뉴 권한 조회 실패", { error, authObjid }); throw error; } } /** * 메뉴 권한 설정 (여러 메뉴) */ static async setMenuPermissions( authObjid: number, permissions: Array<{ menuObjid: number; createYn: string; readYn: string; updateYn: string; deleteYn: string; }>, writer: string ): Promise { try { // 기존 권한 삭제 await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [ authObjid, ]); // 새로운 권한 삽입 if (permissions.length > 0) { const values = permissions .map( (_, index) => `(nextval('seq_rel_menu_auth'), $${index * 5 + 2}, $1, $${index * 5 + 3}, $${index * 5 + 4}, $${index * 5 + 5}, $${index * 5 + 6}, $${permissions.length * 5 + 2}, NOW())` ) .join(", "); const params = permissions.flatMap((p) => [ p.menuObjid, p.createYn, p.readYn, p.updateYn, p.deleteYn, ]); const sql = ` INSERT INTO rel_menu_auth (objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, writer, regdate) VALUES ${values} `; await query(sql, [authObjid, ...params, writer]); } logger.info("메뉴 권한 설정 성공", { authObjid, count: permissions.length, }); } catch (error) { logger.error("메뉴 권한 설정 실패", { error, authObjid, permissions }); throw error; } } /** * 사용자가 속한 권한 그룹 목록 조회 */ static async getUserRoleGroups( userId: string, companyCode: string ): Promise { try { const sql = ` SELECT am.objid, am.auth_name AS "authName", am.auth_code AS "authCode", am.company_code AS "companyCode", am.status, am.writer, am.regdate FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = $1 AND am.company_code = $2 AND am.status = 'active' ORDER BY am.auth_name `; const result = await query(sql, [userId, companyCode]); return result; } catch (error) { logger.error("사용자 권한 그룹 조회 실패", { error, userId, companyCode, }); throw error; } } /** * 전체 메뉴 목록 조회 (권한 설정용) * * @param companyCode - 회사 코드 * - undefined: 최고 관리자 - 모든 회사의 모든 메뉴 조회 * - "*": 최고 관리자의 공통 메뉴만 조회 (최고 관리자 전용) * - "COMPANY_X": 해당 회사 메뉴만 조회 (공통 메뉴 제외) * * 중요: * - 공통 메뉴(company_code = "*")는 최고 관리자 전용 메뉴입니다. * - menu_type = 2 (화면)는 제외하고 메뉴만 조회합니다. */ static async getAllMenus(companyCode?: string): Promise { try { logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode }); let whereConditions: string[] = [ "status = 'active'", "menu_type != 2" // 화면 제외, 메뉴만 조회 ]; const params: any[] = []; let paramIndex = 1; // 회사 코드에 따른 필터링 if (companyCode === undefined) { // 최고 관리자: 모든 메뉴 조회 logger.info("📋 최고 관리자 모드: 모든 메뉴 조회"); } else if (companyCode === "*") { // 공통 메뉴만 조회 whereConditions.push(`company_code = $${paramIndex}`); params.push("*"); paramIndex++; logger.info("📋 공통 메뉴만 조회"); } else { // 특정 회사: 해당 회사 메뉴 + 공통 메뉴 조회 whereConditions.push(`(company_code = $${paramIndex} OR company_code = '*')`); params.push(companyCode); paramIndex++; logger.info("📋 회사별 필터 적용 (해당 회사 + 공통 메뉴)", { companyCode }); } const whereClause = whereConditions.join(" AND "); const sql = ` SELECT objid, menu_name_kor AS "menuName", menu_name_eng AS "menuNameEng", menu_code AS "menuCode", menu_url AS "menuUrl", CAST(menu_type AS TEXT) AS "menuType", parent_obj_id AS "parentObjid", seq AS "sortOrder", company_code AS "companyCode" FROM menu_info WHERE ${whereClause} ORDER BY CASE WHEN parent_obj_id = 0 OR parent_obj_id IS NULL THEN 0 ELSE 1 END, seq, menu_name_kor `; logger.info("🔍 SQL 쿼리 실행", { whereClause, params, sql: sql.substring(0, 200) + "...", }); const result = await query(sql, params); logger.info("✅ 메뉴 목록 조회 성공", { count: result.length, companyCode: companyCode || "전체", companyCodes: [...new Set(result.map((m) => m.companyCode))], menus: result.slice(0, 5).map((m) => ({ objid: m.objid, name: m.menuName, code: m.menuCode, companyCode: m.companyCode, })), }); return result; } catch (error) { logger.error("❌ 메뉴 목록 조회 실패", { error, companyCode }); throw error; } } }