diff --git a/backend-node/src/controllers/auditLogController.ts b/backend-node/src/controllers/auditLogController.ts index 828529bd..cd59a435 100644 --- a/backend-node/src/controllers/auditLogController.ts +++ b/backend-node/src/controllers/auditLogController.ts @@ -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 => { + 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: "감사 로그 기록 실패" }); + } +}; diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index cb6df7c4..a232c03d 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -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, diff --git a/backend-node/src/routes/auditLogRoutes.ts b/backend-node/src/routes/auditLogRoutes.ts index 0d219018..4c6392a8 100644 --- a/backend-node/src/routes/auditLogRoutes.ts +++ b/backend-node/src/routes/auditLogRoutes.ts @@ -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; diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index f8190ce7..0d7840d1 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -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}개)` ); diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 83c55777..51060ce2 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -429,29 +429,31 @@ export function TabsWidget({ })) as any; return ( - ( - - )} - /> +
+ ( + + )} + /> +
); }; @@ -496,7 +498,7 @@ export function TabsWidget({ -
+
{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)} diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 1bc797d8..d5954c6b 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -720,29 +720,29 @@ export const SplitPanelLayoutComponent: React.FC }, [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(), }; // 계층 구조 빌드 함수 (트리 구조 유지) diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index ad42938c..efd76407 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -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);