From 435f60dcac09002c706f99bc59b3f9fe27edc844 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 19 Feb 2026 11:13:40 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/training/common/utils/HeaderUtil.java | 23 ++++ .../training/config/api/ApiLogFunction.java | 41 ++++-- .../config/api/ApiResponseAdvice.java | 59 +++++++-- .../training/log/AuditLogApiController.java | 99 ++++++++++++++ .../training/log/ErrorLogApiController.java | 40 ++++++ .../cd/training/log/dto/AuditLogDto.java | 36 ++++++ .../kamco/cd/training/log/dto/EventType.java | 23 +++- .../postgres/core/AuditLogCoreService.java | 6 + .../postgres/entity/AuditLogEntity.java | 37 +++++- .../log/AuditLogRepositoryCustom.java | 4 + .../log/AuditLogRepositoryImpl.java | 122 +++++++++++++++--- .../log/ErrorLogRepositoryImpl.java | 26 ++-- 12 files changed, 460 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/kamco/cd/training/common/utils/HeaderUtil.java create mode 100644 src/main/java/com/kamco/cd/training/log/AuditLogApiController.java create mode 100644 src/main/java/com/kamco/cd/training/log/ErrorLogApiController.java diff --git a/src/main/java/com/kamco/cd/training/common/utils/HeaderUtil.java b/src/main/java/com/kamco/cd/training/common/utils/HeaderUtil.java new file mode 100644 index 0000000..1b354f0 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/common/utils/HeaderUtil.java @@ -0,0 +1,23 @@ +package com.kamco.cd.training.common.utils; + +import jakarta.servlet.http.HttpServletRequest; + +public final class HeaderUtil { + + private HeaderUtil() {} + + /** 특정 Header 값 조회 */ + public static String get(HttpServletRequest request, String headerName) { + if (request == null || headerName == null) { + return null; + } + + String value = request.getHeader(headerName); + return (value != null && !value.isBlank()) ? value : null; + } + + /** 필수 Header 조회 (없으면 null) */ + public static String getRequired(HttpServletRequest request, String headerName) { + return get(request, headerName); + } +} diff --git a/src/main/java/com/kamco/cd/training/config/api/ApiLogFunction.java b/src/main/java/com/kamco/cd/training/config/api/ApiLogFunction.java index ee69df6..3a9621c 100644 --- a/src/main/java/com/kamco/cd/training/config/api/ApiLogFunction.java +++ b/src/main/java/com/kamco/cd/training/config/api/ApiLogFunction.java @@ -5,11 +5,14 @@ import com.kamco.cd.training.log.dto.EventType; import com.kamco.cd.training.menu.dto.MenuDto; import jakarta.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.util.ContentCachingRequestWrapper; +@Slf4j public class ApiLogFunction { // 클라이언트 IP 추출 @@ -34,6 +37,14 @@ public class ApiLogFunction { return ip; } + public static String getXFowardedForIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip != null) { + ip = ip.split(",")[0].trim(); + } + return ip; + } + // 사용자 ID 추출 예시 (Spring Security 기준) public static String getUserId(HttpServletRequest request) { try { @@ -47,20 +58,20 @@ public class ApiLogFunction { String method = request.getMethod().toUpperCase(); String uri = request.getRequestURI().toLowerCase(); - // URL 기반 DOWNLOAD/PRINT 분류 + // URL 기반 DOWNLOAD/PRINT 분류 -> /download는 FileDownloadInterceptor로 옮김 if (uri.contains("/download") || uri.contains("/export")) { return EventType.DOWNLOAD; } if (uri.contains("/print")) { - return EventType.PRINT; + return EventType.OTHER; } // 일반 CRUD return switch (method) { - case "POST" -> EventType.CREATE; - case "GET" -> EventType.READ; - case "DELETE" -> EventType.DELETE; - case "PUT", "PATCH" -> EventType.UPDATE; + case "POST" -> EventType.ADDED; + case "GET" -> EventType.LIST; + case "DELETE" -> EventType.REMOVE; + case "PUT", "PATCH" -> EventType.MODIFIED; default -> EventType.OTHER; }; } @@ -121,12 +132,22 @@ public class ApiLogFunction { public static String getUriMenuInfo(List menuList, String uri) { - MenuDto.Basic m = + String normalizedUri = uri.replace("/api", ""); + MenuDto.Basic basic = menuList.stream() - .filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl())) - .findFirst() + .filter( + menu -> menu.getMenuUrl() != null && normalizedUri.startsWith(menu.getMenuUrl())) + .max(Comparator.comparingInt(m -> m.getMenuUrl().length())) .orElse(null); - return m != null ? m.getMenuUid() : "SYSTEM"; + return basic != null ? basic.getMenuUid() : "SYSTEM"; + } + + public static String cutRequestBody(String value) { + int MAX_LEN = 255; + if (value == null) { + return null; + } + return value.length() <= MAX_LEN ? value : value.substring(0, MAX_LEN); } } diff --git a/src/main/java/com/kamco/cd/training/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/training/config/api/ApiResponseAdvice.java index 80721a9..6f1ea27 100644 --- a/src/main/java/com/kamco/cd/training/config/api/ApiResponseAdvice.java +++ b/src/main/java/com/kamco/cd/training/config/api/ApiResponseAdvice.java @@ -2,10 +2,17 @@ package com.kamco.cd.training.config.api; import com.fasterxml.jackson.databind.ObjectMapper; import com.kamco.cd.training.auth.CustomUserDetails; +import com.kamco.cd.training.common.utils.HeaderUtil; +import com.kamco.cd.training.log.dto.EventType; +import com.kamco.cd.training.menu.dto.MenuDto; import com.kamco.cd.training.menu.service.MenuService; import com.kamco.cd.training.postgres.entity.AuditLogEntity; import com.kamco.cd.training.postgres.repository.log.AuditLogRepository; import jakarta.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; @@ -23,6 +30,7 @@ import org.springframework.web.util.ContentCachingRequestWrapper; * *

createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT */ +@Slf4j @RestControllerAdvice public class ApiResponseAdvice implements ResponseBodyAdvice { @@ -61,12 +69,27 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { if (body instanceof ApiResponseDto apiResponse) { response.setStatusCode(apiResponse.getHttpStatus()); - String ip = ApiLogFunction.getClientIp(servletRequest); - Long userid = null; + String actionType = HeaderUtil.get(servletRequest, "kamco-action-type"); + // actionType 이 없으면 로그 저장하지 않기 || download 는 FileDownloadInterceptor 에서 하기 + // (file down URL prefix 추가는 WebConfig.java 에 하기) + if (actionType == null || actionType.equalsIgnoreCase("download")) { + return body; + } - if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth - && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) { - userid = customUserDetails.getMember().getId(); + String ip = + Optional.ofNullable(HeaderUtil.get(servletRequest, "kamco-user-ip")) + .orElseGet(() -> ApiLogFunction.getXFowardedForIp(servletRequest)); + Long userid = null; + String loginAttemptId = null; + + // 로그인 시도할 때 + if (servletRequest.getRequestURI().contains("/api/auth/signin")) { + loginAttemptId = HeaderUtil.get(servletRequest, "kamco-login-attempt-id"); + } else { + if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth + && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) { + userid = customUserDetails.getMember().getId(); + } } String requestBody; @@ -84,17 +107,33 @@ public class ApiResponseAdvice implements ResponseBodyAdvice { requestBody = maskSensitiveFields(requestBody); } + List list = menuService.getFindAll(); + List result = + list.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, MenuDto.Basic.class); + } else if (item instanceof MenuDto.Basic dto) { + return dto; + } else { + throw new IllegalStateException("Unsupported cache type: " + item.getClass()); + } + }) + .toList(); + AuditLogEntity log = new AuditLogEntity( userid, - ApiLogFunction.getEventType(servletRequest), + EventType.fromName(actionType), ApiLogFunction.isSuccessFail(apiResponse), - ApiLogFunction.getUriMenuInfo( - menuService.getFindAll(), servletRequest.getRequestURI()), + ApiLogFunction.getUriMenuInfo(result, servletRequest.getRequestURI()), ip, servletRequest.getRequestURI(), - requestBody, - apiResponse.getErrorLogUid()); + ApiLogFunction.cutRequestBody(requestBody), + apiResponse.getErrorLogUid(), + null, + loginAttemptId); auditLogRepository.save(log); } diff --git a/src/main/java/com/kamco/cd/training/log/AuditLogApiController.java b/src/main/java/com/kamco/cd/training/log/AuditLogApiController.java new file mode 100644 index 0000000..b68f6f7 --- /dev/null +++ b/src/main/java/com/kamco/cd/training/log/AuditLogApiController.java @@ -0,0 +1,99 @@ +package com.kamco.cd.training.log; + +import com.kamco.cd.training.config.api.ApiResponseDto; +import com.kamco.cd.training.log.dto.AuditLogDto; +import com.kamco.cd.training.log.service.AuditLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "감사 로그", description = "감사 로그 관리 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/logs/audit") +public class AuditLogApiController { + + private final AuditLogService auditLogService; + + @Operation(summary = "일자별 로그 조회") + @GetMapping("/daily") + public ApiResponseDto> getDailyLogs( + @RequestParam(required = false) LocalDate startDate, + @RequestParam(required = false) LocalDate endDate, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + + Page result = + auditLogService.getLogByDaily(searchReq, startDate, endDate); + + return ApiResponseDto.ok(result); + } + + @Operation(summary = "일자별 로그 상세") + @GetMapping("/daily/result") + public ApiResponseDto> getDailyResultLogs( + @RequestParam LocalDate logDate, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + Page result = auditLogService.getLogByDailyResult(searchReq, logDate); + + return ApiResponseDto.ok(result); + } + + @Operation(summary = "메뉴별 로그 조회") + @GetMapping("/menu") + public ApiResponseDto> getMenuLogs( + @RequestParam(required = false) String searchValue, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + Page result = auditLogService.getLogByMenu(searchReq, searchValue); + + return ApiResponseDto.ok(result); + } + + @Operation(summary = "메뉴별 로그 상세") + @GetMapping("/menu/result") + public ApiResponseDto> getMenuResultLogs( + @RequestParam String menuId, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + Page result = auditLogService.getLogByMenuResult(searchReq, menuId); + + return ApiResponseDto.ok(result); + } + + @Operation(summary = "사용자별 로그 조회") + @GetMapping("/account") + public ApiResponseDto> getAccountLogs( + @RequestParam(required = false) String searchValue, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + Page result = + auditLogService.getLogByAccount(searchReq, searchValue); + + return ApiResponseDto.ok(result); + } + + @Operation(summary = "사용자별 로그 상세") + @GetMapping("/account/result") + public ApiResponseDto> getAccountResultLogs( + @RequestParam Long userUid, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + AuditLogDto.searchReq searchReq = new AuditLogDto.searchReq(page, size, "created_dttm,desc"); + Page result = auditLogService.getLogByAccountResult(searchReq, userUid); + + return ApiResponseDto.ok(result); + } +} diff --git a/src/main/java/com/kamco/cd/training/log/ErrorLogApiController.java b/src/main/java/com/kamco/cd/training/log/ErrorLogApiController.java new file mode 100644 index 0000000..b0932ba --- /dev/null +++ b/src/main/java/com/kamco/cd/training/log/ErrorLogApiController.java @@ -0,0 +1,40 @@ +package com.kamco.cd.training.log; + +import com.kamco.cd.training.config.api.ApiResponseDto; +import com.kamco.cd.training.log.dto.ErrorLogDto; +import com.kamco.cd.training.log.dto.EventType; +import com.kamco.cd.training.log.service.ErrorLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "에러 로그", description = "에러 로그 관리 API") +@RequiredArgsConstructor +@RestController +@RequestMapping({"/api/logs/system"}) +public class ErrorLogApiController { + + private final ErrorLogService errorLogService; + + @Operation(summary = "에러로그 조회") + @GetMapping("/error") + public ApiResponseDto> getErrorLogs( + @RequestParam(required = false) ErrorLogDto.LogErrorLevel logErrorLevel, + @RequestParam(required = false) EventType eventType, + @RequestParam(required = false) LocalDate startDate, + @RequestParam(required = false) LocalDate endDate, + @RequestParam int page, + @RequestParam(defaultValue = "20") int size) { + ErrorLogDto.ErrorSearchReq searchReq = + new ErrorLogDto.ErrorSearchReq( + logErrorLevel, eventType, startDate, endDate, page, size, "created_dttm,desc"); + Page result = errorLogService.findLogByError(searchReq); + return ApiResponseDto.ok(result); + } +} diff --git a/src/main/java/com/kamco/cd/training/log/dto/AuditLogDto.java b/src/main/java/com/kamco/cd/training/log/dto/AuditLogDto.java index 2fc78b8..a54a1ce 100644 --- a/src/main/java/com/kamco/cd/training/log/dto/AuditLogDto.java +++ b/src/main/java/com/kamco/cd/training/log/dto/AuditLogDto.java @@ -3,7 +3,9 @@ package com.kamco.cd.training.log.dto; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; import java.time.ZonedDateTime; +import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -58,6 +60,7 @@ public class AuditLogDto { @Getter @AllArgsConstructor public static class AuditCommon { + private int readCount; private int cudCount; private int printCount; @@ -68,6 +71,7 @@ public class AuditLogDto { @Schema(name = "DailyAuditList", description = "일자별 목록") @Getter public static class DailyAuditList extends AuditCommon { + private final String baseDate; public DailyAuditList( @@ -85,6 +89,7 @@ public class AuditLogDto { @Schema(name = "MenuAuditList", description = "메뉴별 목록") @Getter public static class MenuAuditList extends AuditCommon { + private final String menuId; private final String menuName; @@ -105,6 +110,7 @@ public class AuditLogDto { @Schema(name = "UserAuditList", description = "사용자별 목록") @Getter public static class UserAuditList extends AuditCommon { + private final Long accountId; private final String loginId; private final String username; @@ -129,6 +135,7 @@ public class AuditLogDto { @Getter @AllArgsConstructor public static class AuditDetail { + private Long logId; private EventType eventType; private LogDetail detail; @@ -137,9 +144,11 @@ public class AuditLogDto { @Schema(name = "DailyDetail", description = "일자별 로그 상세") @Getter public static class DailyDetail extends AuditDetail { + private final String userName; private final String loginId; private final String menuName; + private final String logDateTime; public DailyDetail( Long logId, @@ -147,17 +156,20 @@ public class AuditLogDto { String loginId, String menuName, EventType eventType, + String logDateTime, LogDetail detail) { super(logId, eventType, detail); this.userName = userName; this.loginId = loginId; this.menuName = menuName; + this.logDateTime = logDateTime; } } @Schema(name = "MenuDetail", description = "메뉴별 로그 상세") @Getter public static class MenuDetail extends AuditDetail { + private final String logDateTime; private final String userName; private final String loginId; @@ -179,6 +191,7 @@ public class AuditLogDto { @Schema(name = "UserDetail", description = "사용자별 로그 상세") @Getter public static class UserDetail extends AuditDetail { + private final String logDateTime; private final String menuNm; @@ -194,6 +207,7 @@ public class AuditLogDto { @Setter @AllArgsConstructor public static class LogDetail { + String serviceName; String parentMenuName; String menuName; @@ -226,4 +240,26 @@ public class AuditLogDto { return PageRequest.of(page, size); } } + + @Getter + @Setter + public static class DownloadReq { + + UUID uuid; + LocalDate startDate; + LocalDate endDate; + String searchValue; + String menuId; + String requestUri; + } + + @Getter + @Setter + @AllArgsConstructor + public static class DownloadRes { + + String name; + String employeeNo; + @JsonFormatDttm ZonedDateTime downloadDttm; + } } diff --git a/src/main/java/com/kamco/cd/training/log/dto/EventType.java b/src/main/java/com/kamco/cd/training/log/dto/EventType.java index bf5707a..cd248b2 100644 --- a/src/main/java/com/kamco/cd/training/log/dto/EventType.java +++ b/src/main/java/com/kamco/cd/training/log/dto/EventType.java @@ -1,22 +1,35 @@ package com.kamco.cd.training.log.dto; +import com.kamco.cd.training.common.utils.enums.CodeExpose; import com.kamco.cd.training.common.utils.enums.EnumType; import lombok.AllArgsConstructor; import lombok.Getter; +@CodeExpose @Getter @AllArgsConstructor public enum EventType implements EnumType { - CREATE("생성"), - READ("조회"), - UPDATE("수정"), - DELETE("삭제"), + LIST("목록"), + DETAIL("상세"), + POPUP("팝업"), + STATUS("상태"), + ADDED("추가"), + MODIFIED("수정"), + REMOVE("삭제"), DOWNLOAD("다운로드"), - PRINT("출력"), + LOGIN("로그인"), OTHER("기타"); private final String desc; + public static EventType fromName(String name) { + try { + return EventType.valueOf(name.toUpperCase()); + } catch (Exception e) { + return OTHER; + } + } + @Override public String getId() { return name(); diff --git a/src/main/java/com/kamco/cd/training/postgres/core/AuditLogCoreService.java b/src/main/java/com/kamco/cd/training/postgres/core/AuditLogCoreService.java index a2a4473..e5fc73c 100644 --- a/src/main/java/com/kamco/cd/training/postgres/core/AuditLogCoreService.java +++ b/src/main/java/com/kamco/cd/training/postgres/core/AuditLogCoreService.java @@ -2,6 +2,7 @@ package com.kamco.cd.training.postgres.core; import com.kamco.cd.training.common.service.BaseCoreService; import com.kamco.cd.training.log.dto.AuditLogDto; +import com.kamco.cd.training.log.dto.AuditLogDto.DownloadReq; import com.kamco.cd.training.postgres.repository.log.AuditLogRepository; import java.time.LocalDate; import lombok.RequiredArgsConstructor; @@ -45,6 +46,11 @@ public class AuditLogCoreService return auditLogRepository.findLogByAccount(searchRange, searchValue); } + public Page findLogByAccount( + AuditLogDto.searchReq searchReq, DownloadReq downloadReq) { + return auditLogRepository.findDownloadLog(searchReq, downloadReq); + } + public Page getLogByDailyResult( AuditLogDto.searchReq searchRange, LocalDate logDate) { return auditLogRepository.findLogByDailyResult(searchRange, logDate); diff --git a/src/main/java/com/kamco/cd/training/postgres/entity/AuditLogEntity.java b/src/main/java/com/kamco/cd/training/postgres/entity/AuditLogEntity.java index d8cf554..84ffbf3 100644 --- a/src/main/java/com/kamco/cd/training/postgres/entity/AuditLogEntity.java +++ b/src/main/java/com/kamco/cd/training/postgres/entity/AuditLogEntity.java @@ -5,6 +5,7 @@ import com.kamco.cd.training.log.dto.EventStatus; import com.kamco.cd.training.log.dto.EventType; import com.kamco.cd.training.postgres.CommonCreateEntity; import jakarta.persistence.*; +import java.util.UUID; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,6 +15,7 @@ import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "tb_audit_log") public class AuditLogEntity extends CommonCreateEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "audit_log_uid", nullable = false) @@ -43,6 +45,12 @@ public class AuditLogEntity extends CommonCreateEntity { @Column(name = "error_log_uid") private Long errorLogUid; + @Column(name = "download_uuid") + private UUID downloadUuid; + + @Column(name = "login_attempt_id") + private String loginAttemptId; + public AuditLogEntity( Long userUid, EventType eventType, @@ -51,7 +59,9 @@ public class AuditLogEntity extends CommonCreateEntity { String ipAddress, String requestUri, String requestBody, - Long errorLogUid) { + Long errorLogUid, + UUID downloadUuid, + String loginAttemptId) { this.userUid = userUid; this.eventType = eventType; this.eventStatus = eventStatus; @@ -60,6 +70,31 @@ public class AuditLogEntity extends CommonCreateEntity { this.requestUri = requestUri; this.requestBody = requestBody; this.errorLogUid = errorLogUid; + this.downloadUuid = downloadUuid; + this.loginAttemptId = loginAttemptId; + } + + /** 파일 다운로드 이력 생성 */ + public static AuditLogEntity forFileDownload( + Long userId, + String requestUri, + String menuUid, + String ip, + int httpStatus, + UUID downloadUuid) { + + return new AuditLogEntity( + userId, + EventType.DOWNLOAD, // 이벤트 타입 고정 + httpStatus < 400 ? EventStatus.SUCCESS : EventStatus.FAILED, // 성공 여부 + menuUid, + ip, + requestUri, + null, // requestBody 없음 + null, // errorLogUid 없음 + downloadUuid, + null // loginAttemptId 없음 + ); } public AuditLogDto.Basic toDto() { diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryCustom.java b/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryCustom.java index 9c64c7a..14f66dc 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryCustom.java @@ -1,6 +1,7 @@ package com.kamco.cd.training.postgres.repository.log; import com.kamco.cd.training.log.dto.AuditLogDto; +import com.kamco.cd.training.log.dto.AuditLogDto.DownloadReq; import java.time.LocalDate; import org.springframework.data.domain.Page; @@ -15,6 +16,9 @@ public interface AuditLogRepositoryCustom { Page findLogByAccount( AuditLogDto.searchReq searchReq, String searchValue); + Page findDownloadLog( + AuditLogDto.searchReq searchReq, DownloadReq downloadReq); + Page findLogByDailyResult( AuditLogDto.searchReq searchReq, LocalDate logDate); diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryImpl.java index f7353ac..0854dcc 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/log/AuditLogRepositoryImpl.java @@ -6,32 +6,42 @@ import static com.kamco.cd.training.postgres.entity.QMemberEntity.memberEntity; import static com.kamco.cd.training.postgres.entity.QMenuEntity.menuEntity; import com.kamco.cd.training.log.dto.AuditLogDto; +import com.kamco.cd.training.log.dto.AuditLogDto.DownloadReq; +import com.kamco.cd.training.log.dto.AuditLogDto.searchReq; import com.kamco.cd.training.log.dto.ErrorLogDto; import com.kamco.cd.training.log.dto.EventStatus; import com.kamco.cd.training.log.dto.EventType; +import com.kamco.cd.training.postgres.entity.AuditLogEntity; import com.kamco.cd.training.postgres.entity.QMenuEntity; +import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.*; import com.querydsl.jpa.impl.JPAQueryFactory; import io.micrometer.common.util.StringUtils; import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; -import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.stereotype.Repository; @Repository -@RequiredArgsConstructor -public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { +public class AuditLogRepositoryImpl extends QuerydslRepositorySupport + implements AuditLogRepositoryCustom { + private static final ZoneId ZONE = ZoneId.of("Asia/Seoul"); private final JPAQueryFactory queryFactory; private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)"); + public AuditLogRepositoryImpl(JPAQueryFactory queryFactory) { + super(AuditLogEntity.class); + this.queryFactory = queryFactory; + } + @Override public Page findLogByDaily( AuditLogDto.searchReq searchReq, LocalDate startDate, LocalDate endDate) { @@ -87,7 +97,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { .from(auditLogEntity) .leftJoin(menuEntity) .on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) - .where(menuNameEquals(searchValue)) + .where(auditLogEntity.menuUid.ne("SYSTEM"), menuNameEquals(searchValue)) .groupBy(auditLogEntity.menuUid) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -128,7 +138,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { .from(auditLogEntity) .leftJoin(memberEntity) .on(auditLogEntity.userUid.eq(memberEntity.id)) - .where(loginIdOrUsernameContains(searchValue)) + .where(auditLogEntity.userUid.isNotNull(), loginIdOrUsernameContains(searchValue)) .groupBy(auditLogEntity.userUid, memberEntity.employeeNo, memberEntity.name) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -147,6 +157,62 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { return new PageImpl<>(foundContent, pageable, countQuery); } + @Override + public Page findDownloadLog( + AuditLogDto.searchReq searchReq, DownloadReq req) { + Pageable pageable = searchReq.toPageable(); + + BooleanBuilder whereBuilder = new BooleanBuilder(); + + whereBuilder.and(auditLogEntity.eventStatus.ne(EventStatus.valueOf("FAILED"))); + whereBuilder.and(auditLogEntity.eventType.eq(EventType.valueOf("DOWNLOAD"))); + + // if (req.getMenuId() != null && !req.getMenuId().isEmpty()) { + // whereBuilder.and(auditLogEntity.menuUid.eq(req.getMenuId())); + // } + + if (req.getUuid() != null) { + whereBuilder.and(auditLogEntity.requestUri.contains(req.getRequestUri())); + whereBuilder.and(auditLogEntity.downloadUuid.eq(req.getUuid())); + } + + if (req.getSearchValue() != null && !req.getSearchValue().isEmpty()) { + whereBuilder.and( + memberEntity + .name + .contains(req.getSearchValue()) + .or(memberEntity.employeeNo.contains(req.getSearchValue()))); + } + + List foundContent = + queryFactory + .select( + Projections.constructor( + AuditLogDto.DownloadRes.class, + memberEntity.name, + memberEntity.employeeNo, + auditLogEntity.createdDate.as("downloadDttm"))) + .from(auditLogEntity) + .leftJoin(memberEntity) + .on(auditLogEntity.userUid.eq(memberEntity.id)) + .where(whereBuilder, createdDateBetween(req.getStartDate(), req.getEndDate())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(auditLogEntity.createdDate.desc()) + .fetch(); + + Long countQuery = + queryFactory + .select(auditLogEntity.userUid.countDistinct()) + .from(auditLogEntity) + .leftJoin(memberEntity) + .on(auditLogEntity.userUid.eq(memberEntity.id)) + .where(whereBuilder, createdDateBetween(req.getStartDate(), req.getEndDate())) + .fetchOne(); + + return new PageImpl<>(foundContent, pageable, countQuery); + } + @Override public Page findLogByDailyResult( AuditLogDto.searchReq searchReq, LocalDate logDate) { @@ -176,6 +242,9 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { memberEntity.employeeNo.as("loginId"), menuEntity.menuNm.as("menuName"), auditLogEntity.eventType.as("eventType"), + Expressions.stringTemplate( + "to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate) + .as("logDateTime"), Projections.constructor( AuditLogDto.LogDetail.class, Expressions.constant("한국자산관리공사"), // serviceName @@ -184,7 +253,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { menuEntity.menuUrl.as("menuUrl"), menuEntity.description.as("menuDescription"), menuEntity.menuOrder.as("sortOrder"), - menuEntity.isUse.as("used")))) + menuEntity.isUse.as("used")))) // TODO .from(auditLogEntity) .leftJoin(menuEntity) .on(auditLogEntity.menuUid.eq(menuEntity.menuUid)) @@ -238,8 +307,8 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { AuditLogDto.MenuDetail.class, auditLogEntity.id.as("logId"), Expressions.stringTemplate( - "to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate) - .as("logDateTime"), // ?? + "to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate) + .as("logDateTime"), memberEntity.name.as("userName"), memberEntity.employeeNo.as("loginId"), auditLogEntity.eventType.as("eventType"), @@ -305,7 +374,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { AuditLogDto.UserDetail.class, auditLogEntity.id.as("logId"), Expressions.stringTemplate( - "to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate) + "to_char({0}, 'YYYY-MM-DD HH24:MI')", auditLogEntity.createdDate) .as("logDateTime"), menuEntity.menuNm.as("menuName"), auditLogEntity.eventType.as("eventType"), @@ -349,12 +418,23 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { if (Objects.isNull(startDate) || Objects.isNull(endDate)) { return null; } - LocalDateTime startDateTime = startDate.atStartOfDay(); - LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime startDateTime = startDate.atStartOfDay(zoneId); + ZonedDateTime endDateTime = endDate.plusDays(1).atStartOfDay(zoneId); return auditLogEntity .createdDate - .goe(ZonedDateTime.from(startDateTime)) - .and(auditLogEntity.createdDate.lt(ZonedDateTime.from(endDateTime))); + .goe(startDateTime) + .and(auditLogEntity.createdDate.lt(endDateTime)); + } + + private BooleanExpression createdDateBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + return null; + } + ZonedDateTime start = startDate.atStartOfDay(ZONE); + ZonedDateTime endExclusive = endDate.plusDays(1).atStartOfDay(ZONE); + + return auditLogEntity.createdDate.goe(start).and(auditLogEntity.createdDate.lt(endExclusive)); } private BooleanExpression menuNameEquals(String searchValue) { @@ -393,11 +473,11 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { } private BooleanExpression eventEndedAtEqDate(LocalDate logDate) { - StringExpression eventEndedDate = - Expressions.stringTemplate("to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate); - LocalDateTime comparisonDate = logDate.atStartOfDay(); + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime start = logDate.atStartOfDay(zoneId); + ZonedDateTime end = logDate.plusDays(1).atStartOfDay(zoneId); - return eventEndedDate.eq(comparisonDate.toString()); + return auditLogEntity.createdDate.goe(start).and(auditLogEntity.createdDate.lt(end)); } private BooleanExpression menuUidEq(String menuUid) { @@ -410,7 +490,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { private NumberExpression readCount() { return new CaseBuilder() - .when(auditLogEntity.eventType.eq(EventType.READ)) + .when(auditLogEntity.eventType.in(EventType.LIST, EventType.DETAIL)) .then(1) .otherwise(0) .sum(); @@ -418,7 +498,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { private NumberExpression cudCount() { return new CaseBuilder() - .when(auditLogEntity.eventType.in(EventType.CREATE, EventType.UPDATE, EventType.DELETE)) + .when(auditLogEntity.eventType.in(EventType.ADDED, EventType.MODIFIED, EventType.REMOVE)) .then(1) .otherwise(0) .sum(); @@ -426,7 +506,7 @@ public class AuditLogRepositoryImpl implements AuditLogRepositoryCustom { private NumberExpression printCount() { return new CaseBuilder() - .when(auditLogEntity.eventType.eq(EventType.PRINT)) + .when(auditLogEntity.eventType.eq(EventType.OTHER)) .then(1) .otherwise(0) .sum(); diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/log/ErrorLogRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/log/ErrorLogRepositoryImpl.java index 97b50a3..41facfe 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/log/ErrorLogRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/log/ErrorLogRepositoryImpl.java @@ -8,29 +8,35 @@ import static com.kamco.cd.training.postgres.entity.QMenuEntity.menuEntity; import com.kamco.cd.training.log.dto.ErrorLogDto; import com.kamco.cd.training.log.dto.EventStatus; import com.kamco.cd.training.log.dto.EventType; +import com.kamco.cd.training.postgres.entity.AuditLogEntity; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; -import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.stereotype.Repository; @Repository -@RequiredArgsConstructor -public class ErrorLogRepositoryImpl implements ErrorLogRepositoryCustom { +public class ErrorLogRepositoryImpl extends QuerydslRepositorySupport + implements ErrorLogRepositoryCustom { private final JPAQueryFactory queryFactory; private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)"); + public ErrorLogRepositoryImpl(JPAQueryFactory queryFactory) { + super(AuditLogEntity.class); + this.queryFactory = queryFactory; + } + @Override public Page findLogByError(ErrorLogDto.ErrorSearchReq searchReq) { Pageable pageable = searchReq.toPageable(); @@ -52,7 +58,7 @@ public class ErrorLogRepositoryImpl implements ErrorLogRepositoryCustom { errorLogEntity.errorMessage.as("errorMessage"), errorLogEntity.stackTrace.as("errorDetail"), Expressions.stringTemplate( - "to_char({0}, 'YYYY-MM-DD')", errorLogEntity.createdDate))) + "to_char({0}, 'YYYY-MM-DD HH24:MI:SS.FF3')", errorLogEntity.createdDate))) .from(errorLogEntity) .leftJoin(auditLogEntity) .on(errorLogEntity.id.eq(auditLogEntity.errorLogUid)) @@ -94,12 +100,14 @@ public class ErrorLogRepositoryImpl implements ErrorLogRepositoryCustom { if (Objects.isNull(startDate) || Objects.isNull(endDate)) { return null; } - LocalDateTime startDateTime = startDate.atStartOfDay(); - LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay(); + + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime startDateTime = startDate.atStartOfDay(zoneId); + ZonedDateTime endDateTime = endDate.plusDays(1).atStartOfDay(zoneId); return auditLogEntity .createdDate - .goe(ZonedDateTime.from(startDateTime)) - .and(auditLogEntity.createdDate.lt(ZonedDateTime.from(endDateTime))); + .goe(startDateTime) + .and(auditLogEntity.createdDate.lt(endDateTime)); } private BooleanExpression eventStatusEqFailed() {