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:
kjs
2026-03-04 13:49:08 +09:00
parent f04d224b09
commit b4d5367e2b
26 changed files with 2620 additions and 140 deletions

View File

@@ -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 ? "사원 정보가 수정되었습니다." : "사원이 등록되었습니다.",