611 lines
16 KiB
TypeScript
611 lines
16 KiB
TypeScript
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<RoleGroup[]> {
|
|
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<RoleGroup>(sql, params);
|
|
logger.info("권한 그룹 조회 결과", { count: result.length });
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("권한 그룹 목록 조회 실패", { error, companyCode, search });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 권한 그룹 상세 조회
|
|
*/
|
|
static async getRoleGroupById(objid: number): Promise<RoleGroup | null> {
|
|
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<RoleGroup>(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<RoleGroup> {
|
|
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<RoleGroup>(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<RoleGroup> {
|
|
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<RoleGroup>(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<void> {
|
|
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<RoleMember[]> {
|
|
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<RoleMember>(sql, [masterObjid]);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("권한 그룹 멤버 조회 실패", { error, masterObjid });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 권한 그룹 멤버 추가 (여러 명)
|
|
*/
|
|
static async addRoleMembers(
|
|
masterObjid: number,
|
|
userIds: string[],
|
|
writer: string
|
|
): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<MenuPermission[]> {
|
|
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<MenuPermission>(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<void> {
|
|
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<RoleGroup[]> {
|
|
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<RoleGroup>(sql, [userId, companyCode]);
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("사용자 권한 그룹 조회 실패", {
|
|
error,
|
|
userId,
|
|
companyCode,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전체 메뉴 목록 조회 (권한 설정용)
|
|
*/
|
|
/**
|
|
* 전체 메뉴 목록 조회 (권한 설정용)
|
|
*/
|
|
static async getAllMenus(companyCode?: string): Promise<any[]> {
|
|
try {
|
|
logger.info("🔍 전체 메뉴 목록 조회 시작", { companyCode });
|
|
|
|
let whereConditions: string[] = ["status = 'active'"];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
// 회사 코드 필터 (선택적)
|
|
// 공통 메뉴(*)와 특정 회사 메뉴를 모두 조회
|
|
// 회사 코드 필터 (선택적)
|
|
if (companyCode) {
|
|
// 특정 회사 메뉴만 조회 (공통 메뉴 제외)
|
|
whereConditions.push(`company_code = $${paramIndex}`);
|
|
params.push(companyCode);
|
|
paramIndex++;
|
|
logger.info("📋 회사 코드 필터 적용 (공통 메뉴 제외)", { companyCode });
|
|
} else {
|
|
logger.info("📋 회사 코드 필터 없음 (전체 조회)");
|
|
}
|
|
|
|
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",
|
|
menu_type AS "menuType",
|
|
parent_obj_id AS "parentObjid",
|
|
seq AS "sortOrder",
|
|
company_code AS "companyCode"
|
|
FROM menu_info
|
|
WHERE ${whereClause}
|
|
ORDER BY seq, menu_name_kor
|
|
`;
|
|
|
|
logger.info("🔍 SQL 쿼리 실행", {
|
|
whereClause,
|
|
params,
|
|
sql: sql.substring(0, 200) + "...",
|
|
});
|
|
|
|
const result = await query<any>(sql, params);
|
|
|
|
logger.info("✅ 메뉴 목록 조회 성공", {
|
|
count: result.length,
|
|
companyCode,
|
|
menus: result.map((m) => ({
|
|
objid: m.objid,
|
|
name: m.menuName,
|
|
code: m.menuCode,
|
|
companyCode: m.companyCode,
|
|
})),
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
logger.error("❌ 메뉴 목록 조회 실패", { error, companyCode });
|
|
throw error;
|
|
}
|
|
}
|
|
}
|