feat: Integrate audit logging for various operations
- Added audit logging functionality across multiple controllers, including menu, user, department, flow, screen, and table management. - Implemented logging for create, update, and delete actions, capturing relevant details such as company code, user information, and changes made. - Enhanced the category tree service with a new endpoint to check if category values are in use, improving data integrity checks. - Updated routes to include new functionalities and ensure proper logging for batch operations and individual record changes. - This integration improves traceability and accountability for data modifications within the application.
This commit is contained in:
@@ -10,6 +10,7 @@ import { EncryptUtil } from "../utils/encryptUtil";
|
||||
import { FileSystemManager } from "../utils/fileSystemManager";
|
||||
import { validateBusinessNumber } from "../utils/businessNumberValidator";
|
||||
import { MenuCopyService } from "../services/menuCopyService";
|
||||
import { auditLogService } from "../services/auditLogService";
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 목록 조회
|
||||
@@ -1177,7 +1178,7 @@ export async function saveMenu(
|
||||
success: true,
|
||||
message: "메뉴가 성공적으로 저장되었습니다.",
|
||||
data: {
|
||||
objid: savedMenu.objid.toString(), // BigInt를 문자열로 변환
|
||||
objid: savedMenu.objid.toString(),
|
||||
menuNameKor: savedMenu.menu_name_kor,
|
||||
menuNameEng: savedMenu.menu_name_eng,
|
||||
menuUrl: savedMenu.menu_url,
|
||||
@@ -1188,6 +1189,20 @@ export async function saveMenu(
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "CREATE",
|
||||
resourceType: "MENU",
|
||||
resourceId: savedMenu.objid?.toString(),
|
||||
resourceName: savedMenu.menu_name_kor,
|
||||
summary: `메뉴 "${savedMenu.menu_name_kor}" 생성`,
|
||||
changes: { after: { menuNameKor: savedMenu.menu_name_kor, menuUrl: savedMenu.menu_url, status: savedMenu.status } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 저장 실패:", error);
|
||||
@@ -1375,6 +1390,23 @@ export async function updateMenu(
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || updatedMenu.company_code || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "UPDATE",
|
||||
resourceType: "MENU",
|
||||
resourceId: updatedMenu.objid?.toString(),
|
||||
resourceName: updatedMenu.menu_name_kor,
|
||||
summary: `메뉴 "${updatedMenu.menu_name_kor}" 수정`,
|
||||
changes: {
|
||||
before: { menuNameKor: currentMenu.menu_name_kor, menuUrl: currentMenu.menu_url, status: currentMenu.status },
|
||||
after: { menuNameKor: updatedMenu.menu_name_kor, menuUrl: updatedMenu.menu_url, status: updatedMenu.status },
|
||||
},
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 수정 실패:", error);
|
||||
@@ -1554,6 +1586,20 @@ export async function deleteMenu(
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: currentMenu.company_code || req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "DELETE",
|
||||
resourceType: "MENU",
|
||||
resourceId: menuObjid.toString(),
|
||||
resourceName: currentMenu.menu_name_kor,
|
||||
summary: `메뉴 "${currentMenu.menu_name_kor}" 삭제 (하위 ${childMenuIds.length}개 포함, 총 ${allMenuIdsToDelete.length}건)`,
|
||||
changes: { before: { menuNameKor: currentMenu.menu_name_kor } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 삭제 실패:", error);
|
||||
@@ -1717,6 +1763,20 @@ export async function deleteMenusBatch(
|
||||
},
|
||||
};
|
||||
|
||||
if (deletedCount > 0) {
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "DELETE",
|
||||
resourceType: "MENU",
|
||||
summary: `메뉴 일괄 삭제: ${deletedCount}개 삭제, ${failedCount}개 실패`,
|
||||
changes: { before: { deletedMenus, failedMenuIds } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 일괄 삭제 실패:", error);
|
||||
@@ -1813,6 +1873,20 @@ export async function toggleMenuStatus(
|
||||
data: result,
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: currentMenu.company_code || req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "STATUS_CHANGE",
|
||||
resourceType: "MENU",
|
||||
resourceId: String(menuId),
|
||||
resourceName: currentMenu.menu_name_kor,
|
||||
summary: `메뉴 "${currentMenu.menu_name_kor}" 상태 변경: ${currentStatus} → ${newStatus}`,
|
||||
changes: { before: { status: currentStatus }, after: { status: newStatus }, fields: ["status"] },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("메뉴 상태 토글 실패:", error);
|
||||
@@ -2442,6 +2516,20 @@ export const changeUserStatus = async (
|
||||
updatedBy: req.user?.userId,
|
||||
});
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: currentUser.company_code || req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "STATUS_CHANGE",
|
||||
resourceType: "USER",
|
||||
resourceId: userId,
|
||||
resourceName: currentUser.user_name,
|
||||
summary: `사용자 "${currentUser.user_name}" 상태 변경: ${currentUser.status} → ${status}`,
|
||||
changes: { before: { status: currentUser.status }, after: { status }, fields: ["status"] },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
result: true,
|
||||
msg: `사용자 상태가 ${status === "active" ? "활성" : "비활성"}으로 변경되었습니다.`,
|
||||
@@ -2579,6 +2667,20 @@ export const saveUser = async (req: AuthenticatedRequest, res: Response) => {
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: userData.companyCode || req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: isExistingUser ? "UPDATE" : "CREATE",
|
||||
resourceType: "USER",
|
||||
resourceId: userData.userId,
|
||||
resourceName: userData.userName,
|
||||
summary: isExistingUser ? `사용자 "${userData.userName}" 정보 수정` : `사용자 "${userData.userName}" 등록`,
|
||||
changes: { after: { userId: userData.userId, userName: userData.userName, deptName: userData.deptName, status: userData.status } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("사용자 저장 실패", { error, userData: req.body });
|
||||
@@ -2769,6 +2871,20 @@ export const createCompany = async (
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: createdCompany.company_code,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "CREATE",
|
||||
resourceType: "COMPANY",
|
||||
resourceId: createdCompany.company_code,
|
||||
resourceName: createdCompany.company_name,
|
||||
summary: `회사 "${createdCompany.company_name}" (${createdCompany.company_code}) 등록`,
|
||||
changes: { after: { company_code: createdCompany.company_code, company_name: createdCompany.company_name } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(201).json(response);
|
||||
} finally {
|
||||
await client.end();
|
||||
@@ -2938,7 +3054,11 @@ export const updateCompany = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Raw Query로 회사 정보 수정
|
||||
const beforeCompany = await queryOne<any>(
|
||||
`SELECT company_name, status FROM company_mng WHERE company_code = $1`,
|
||||
[companyCode]
|
||||
);
|
||||
|
||||
const result = await query<any>(
|
||||
`UPDATE company_mng
|
||||
SET
|
||||
@@ -2994,6 +3114,23 @@ export const updateCompany = async (
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: updatedCompany.company_code,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "UPDATE",
|
||||
resourceType: "COMPANY",
|
||||
resourceId: updatedCompany.company_code,
|
||||
resourceName: updatedCompany.company_name,
|
||||
summary: `회사 "${updatedCompany.company_name}" 정보 수정`,
|
||||
changes: {
|
||||
before: { company_name: beforeCompany?.company_name, status: beforeCompany?.status },
|
||||
after: { company_name: updatedCompany.company_name, status: updatedCompany.status },
|
||||
},
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("회사 정보 수정 실패", { error, body: req.body });
|
||||
@@ -3055,6 +3192,20 @@ export const deleteCompany = async (
|
||||
},
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: deletedCompany.company_code,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "DELETE",
|
||||
resourceType: "COMPANY",
|
||||
resourceId: deletedCompany.company_code,
|
||||
resourceName: deletedCompany.company_name,
|
||||
summary: `회사 "${deletedCompany.company_name}" (${deletedCompany.company_code}) 삭제`,
|
||||
changes: { before: { company_code: deletedCompany.company_code, company_name: deletedCompany.company_name } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("회사 삭제 실패", { error });
|
||||
@@ -3221,6 +3372,20 @@ export const updateProfile = async (
|
||||
: null,
|
||||
};
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "UPDATE",
|
||||
resourceType: "USER",
|
||||
resourceId: userId,
|
||||
resourceName: updatedUser?.user_name || "",
|
||||
summary: `프로필 수정 (${updateFields.length}개 항목)`,
|
||||
changes: { after: { userName, email, tel, cellPhone, locale } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
result: true,
|
||||
message: "프로필이 성공적으로 업데이트되었습니다.",
|
||||
@@ -3334,6 +3499,20 @@ export const resetUserPassword = async (
|
||||
updatedBy: req.user?.userId,
|
||||
});
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: "UPDATE",
|
||||
resourceType: "USER",
|
||||
resourceId: userId,
|
||||
resourceName: currentUser.user_name,
|
||||
summary: `사용자 "${currentUser.user_name}" 비밀번호 초기화`,
|
||||
changes: { fields: ["user_password"] },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: true,
|
||||
@@ -3535,6 +3714,19 @@ export async function copyMenu(
|
||||
|
||||
logger.info("✅ 메뉴 복사 API 성공");
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || userId,
|
||||
userName: req.user?.userName || "",
|
||||
action: "COPY",
|
||||
resourceType: "MENU",
|
||||
resourceId: menuObjid,
|
||||
summary: `메뉴(${menuObjid}) → 회사 "${targetCompanyCode}"로 복사`,
|
||||
changes: { after: { targetCompanyCode, menuObjid } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "메뉴 복사 완료",
|
||||
@@ -3849,6 +4041,20 @@ export const saveUserWithDept = async (
|
||||
isUpdate: isExistingUser,
|
||||
});
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: isExistingUser ? "UPDATE" : "CREATE",
|
||||
resourceType: "USER",
|
||||
resourceId: userInfo.user_id,
|
||||
resourceName: userInfo.user_name,
|
||||
summary: `사용자 "${userInfo.user_name}" ${isExistingUser ? "수정" : "등록"} (부서: ${mainDept?.dept_name || "없음"})`,
|
||||
changes: { after: { userName: userInfo.user_name, email: userInfo.email, deptName: mainDept?.dept_name, status: userInfo.status } },
|
||||
ipAddress: (req as any).ip,
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: isExistingUser ? "사원 정보가 수정되었습니다." : "사원이 등록되었습니다.",
|
||||
|
||||
Reference in New Issue
Block a user