feat: add createAuditLog endpoint and integrate audit logging for screen copy operations
- Implemented a new endpoint for creating audit logs directly from the frontend, allowing for detailed tracking of actions related to screen copying. - Enhanced the existing screen management functionality by removing redundant audit logging within the copy operations and centralizing it through the new createAuditLog endpoint. - Updated the frontend to log group copy actions, capturing relevant details such as resource type, resource ID, and changes made during the operation. Made-with: Cursor
This commit is contained in:
parent
014979bebf
commit
f65b57410c
|
|
@ -1,6 +1,6 @@
|
|||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||
import { auditLogService } from "../services/auditLogService";
|
||||
import { auditLogService, getClientIp, AuditAction, AuditResourceType } from "../services/auditLogService";
|
||||
import { query } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
|
|
@ -137,3 +137,40 @@ export const getAuditLogUsers = async (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 프론트엔드에서 직접 감사 로그 기록 (그룹 복제 등 프론트 오케스트레이션 작업용)
|
||||
*/
|
||||
export const createAuditLog = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { action, resourceType, resourceId, resourceName, tableName, summary, changes } = req.body;
|
||||
|
||||
if (!action || !resourceType) {
|
||||
res.status(400).json({ success: false, message: "action, resourceType은 필수입니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
await auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: action as AuditAction,
|
||||
resourceType: resourceType as AuditResourceType,
|
||||
resourceId: resourceId || undefined,
|
||||
resourceName: resourceName || undefined,
|
||||
tableName: tableName || undefined,
|
||||
summary: summary || undefined,
|
||||
changes: changes || undefined,
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
logger.error("감사 로그 기록 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: "감사 로그 기록 실패" });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -614,20 +614,6 @@ export const copyScreenWithModals = async (
|
|||
modalScreens: modalScreens || [],
|
||||
});
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: targetCompanyCode || companyCode,
|
||||
userId: userId || "",
|
||||
userName: (req.user as any)?.userName || "",
|
||||
action: "COPY",
|
||||
resourceType: "SCREEN",
|
||||
resourceId: id,
|
||||
resourceName: mainScreen?.screenName,
|
||||
summary: `화면 일괄 복사 (메인 1개 + 모달 ${result.modalScreens.length}개, 원본 ID:${id})`,
|
||||
changes: { after: { sourceScreenId: id, targetCompanyCode, mainScreenName: mainScreen?.screenName } },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
|
|
@ -663,20 +649,6 @@ export const copyScreen = async (
|
|||
}
|
||||
);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: userId || "",
|
||||
userName: (req.user as any)?.userName || "",
|
||||
action: "COPY",
|
||||
resourceType: "SCREEN",
|
||||
resourceId: String(copiedScreen?.screenId || ""),
|
||||
resourceName: screenName,
|
||||
summary: `화면 "${screenName}" 복사 (원본 ID:${id})`,
|
||||
changes: { after: { sourceScreenId: id, screenName, screenCode } },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: copiedScreen,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { Router } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { getAuditLogs, getAuditLogStats, getAuditLogUsers } from "../controllers/auditLogController";
|
||||
import { getAuditLogs, getAuditLogStats, getAuditLogUsers, createAuditLog } from "../controllers/auditLogController";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", authenticateToken, getAuditLogs);
|
||||
router.get("/stats", authenticateToken, getAuditLogStats);
|
||||
router.get("/users", authenticateToken, getAuditLogUsers);
|
||||
router.post("/", authenticateToken, createAuditLog);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1165,6 +1165,28 @@ export default function CopyScreenModal({
|
|||
}
|
||||
}
|
||||
|
||||
// 그룹 복제 요약 감사 로그 1건 기록
|
||||
try {
|
||||
await apiClient.post("/audit-log", {
|
||||
action: "COPY",
|
||||
resourceType: "SCREEN",
|
||||
resourceId: String(sourceGroup.id),
|
||||
resourceName: sourceGroup.group_name,
|
||||
summary: `그룹 "${sourceGroup.group_name}" → "${rootGroupName}" 복제 (그룹 ${stats.groups}개, 화면 ${stats.screens}개)${finalCompanyCode !== sourceGroup.company_code ? ` [${sourceGroup.company_code} → ${finalCompanyCode}]` : ""}`,
|
||||
changes: {
|
||||
after: {
|
||||
원본그룹: sourceGroup.group_name,
|
||||
대상그룹: rootGroupName,
|
||||
복제그룹수: stats.groups,
|
||||
복제화면수: stats.screens,
|
||||
대상회사: finalCompanyCode,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (auditError) {
|
||||
console.warn("그룹 복제 감사 로그 기록 실패 (무시):", auditError);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`그룹 복제가 완료되었습니다! (그룹 ${stats.groups}개 + 화면 ${stats.screens}개)`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -429,29 +429,31 @@ export function TabsWidget({
|
|||
})) as any;
|
||||
|
||||
return (
|
||||
<ResponsiveGridRenderer
|
||||
components={componentDataList}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={(comp) => (
|
||||
<DynamicComponentRenderer
|
||||
{...restProps}
|
||||
component={comp}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
menuObjid={menuObjid}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
{...(screenInfoMap[tab.id]
|
||||
? { tableName: screenInfoMap[tab.id].tableName, screenId: screenInfoMap[tab.id].id }
|
||||
: {})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<ResponsiveGridRenderer
|
||||
components={componentDataList}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={(comp) => (
|
||||
<DynamicComponentRenderer
|
||||
{...restProps}
|
||||
component={comp}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
menuObjid={menuObjid}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
{...(screenInfoMap[tab.id]
|
||||
? { tableName: screenInfoMap[tab.id].tableName, screenId: screenInfoMap[tab.id].id }
|
||||
: {})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -496,7 +498,7 @@ export function TabsWidget({
|
|||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1 overflow-auto">
|
||||
<div className="relative flex flex-1 flex-col overflow-auto">
|
||||
{visibleTabs.map((tab) => {
|
||||
const shouldRender = mountedTabs.has(tab.id);
|
||||
const isActive = selectedTab === tab.id;
|
||||
|
|
@ -506,7 +508,7 @@ export function TabsWidget({
|
|||
key={tab.id}
|
||||
value={tab.id}
|
||||
forceMount
|
||||
className={cn("h-full overflow-auto", !isActive && "hidden")}
|
||||
className={cn("flex min-h-0 flex-1 flex-col overflow-auto", !isActive && "hidden")}
|
||||
>
|
||||
{shouldRender && renderTabContent(tab)}
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -720,29 +720,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}, [leftData, leftGroupSumConfig]);
|
||||
|
||||
// 컴포넌트 스타일
|
||||
// height 처리: 이미 px 단위면 그대로, 숫자면 px 추가
|
||||
// height: component.size?.height 우선, 없으면 component.style?.height, 기본 600px
|
||||
const getHeightValue = () => {
|
||||
const sizeH = component.size?.height;
|
||||
if (sizeH && typeof sizeH === "number" && sizeH > 0) return `${sizeH}px`;
|
||||
const height = component.style?.height;
|
||||
if (!height) return "600px";
|
||||
if (typeof height === "string") return height; // 이미 '540px' 형태
|
||||
return `${height}px`; // 숫자면 px 추가
|
||||
if (typeof height === "string") return height;
|
||||
return `${height}px`;
|
||||
};
|
||||
|
||||
const componentStyle: React.CSSProperties = isDesignMode
|
||||
? {
|
||||
position: "absolute",
|
||||
left: `${component.style?.positionX || 0}px`,
|
||||
top: `${component.style?.positionY || 0}px`,
|
||||
width: "100%",
|
||||
height: getHeightValue(),
|
||||
zIndex: component.style?.positionZ || 1,
|
||||
height: "100%",
|
||||
minHeight: getHeightValue(),
|
||||
cursor: "pointer",
|
||||
border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb",
|
||||
}
|
||||
: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: getHeightValue(),
|
||||
height: "100%",
|
||||
minHeight: getHeightValue(),
|
||||
};
|
||||
|
||||
// 계층 구조 빌드 함수 (트리 구조 유지)
|
||||
|
|
|
|||
|
|
@ -324,15 +324,12 @@ const TabsDesignEditor: React.FC<{
|
|||
const isDragging = draggingCompId === comp.id;
|
||||
const isResizing = resizingCompId === comp.id;
|
||||
|
||||
// 드래그/리사이즈 중 표시할 크기
|
||||
// resizeSize가 있고 해당 컴포넌트이면 resizeSize 우선 사용 (레이아웃 업데이트 반영 전까지)
|
||||
const compWidth = comp.size?.width || 200;
|
||||
const compHeight = comp.size?.height || 100;
|
||||
const isResizingThis = (resizingCompId === comp.id || lastResizedCompId === comp.id) && resizeSize;
|
||||
const displayWidth = isResizingThis ? resizeSize!.width : compWidth;
|
||||
const displayHeight = isResizingThis ? resizeSize!.height : compHeight;
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
|
|
@ -344,7 +341,6 @@ const TabsDesignEditor: React.FC<{
|
|||
style: comp.style || {},
|
||||
};
|
||||
|
||||
// 드래그 중인 컴포넌트는 dragPosition 사용, 아니면 저장된 position 사용
|
||||
const displayX = isDragging && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
||||
const displayY = isDragging && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue